jonnifyfsharp

Dive C2.01.3

Namespaces, members & file order

The organisational rules — and the one that catches every C# developer on day one.

The previous dives moved behaviour into modules. This one covers how those modules are arranged and reached: the difference between a namespace and a module, [<AutoOpen>] to make a module's names available without an open, let private for the private a C# developer expects, and type members — the way to keep dot-syntax (vec.Length) when it reads better than a function call. Then the rule with no C# equivalent: F# compiles top to bottom with no forward references, so a name must be defined before it is used, and the .fsproj lists files in the order they compile. Declaration order and file order are part of the program.

C2.01.3 · part 1

Namespaces, access, and AutoOpen

A namespace is a name for a group of types and modules; it holds no values directly. A module holds values and functions and can itself sit inside a namespace. The mental model a C# developer already has carries over: a namespace groups, a module is the static-class-shaped container that holds the code.

Three smaller controls round out the picture. let private hides a binding from outside its module — the private keyword a C# developer reaches for. [<AutoOpen>] on a module makes its names available without an open — useful for a module of operators or helpers meant to be ambient.

module Geometry =
    let pi = 3.14159
    let private secret = 42        // hidden outside Geometry
    let area r = pi * r * r

open Geometry
area 2.0                          // val it : float = 12.56636

[<AutoOpen>]
module Helpers =
    let twice x = x * 2

twice 21                          // val it : int = 42  (no open needed)
namespace
groups types and modules; holds no values directly — like a C# namespace.
module
holds values and functions; the container behaviour lives in.
let private
restricts a binding to its enclosing module — the private a C# dev expects.
[<AutoOpen>]
makes a module's names available without an open.
C2.01.3 · part 2

Type members: keeping dot-syntax

Most behaviour belongs in a module as a function. But some reads better as a property on the value itself — a vector's length is naturally vec.Length, not Vec.length vec. F# allows that with a type member: a member written on the type, with v naming the instance — F#'s explicit stand-in for the C# this. It is the same record as before, with a computed property attached.

C#

struct Vec
{
    public double X, Y;
    public double Length =>
        Math.Sqrt(X * X + Y * Y);
}
// new Vec{X=3,Y=4}.Length -> 5.0

F#

type Vec =
    { X: float; Y: float }
    member v.Length =
        sqrt (v.X * v.X + v.Y * v.Y)

{ X = 3.0; Y = 4.0 }.Length       // val it : float = 5.0
The instance gets a name. member v.Length binds the value to v — whatever C# would reach through this, F# reaches through the name you chose. Use a member when the dot reads better; use a module function for everything else.
C2.01.3 · part 3

The big one: declaration order is significant

In C#, two methods can call each other and the order they appear in the file does not matter; the compiler resolves names across the whole type. F# does not work that way. It reads a file top to bottom, and a name must be defined before it is used — there are no forward references. The same rule scales up to files: the .fsproj lists source files in compile order, and a type defined in a later file is not visible to an earlier one.

Toggle the order of two bindings below. When total comes before the values it adds, the compiler reports FS0039 — the name is not yet defined. Reorder so the values come first, and it compiles. The readout shows the exact compiler outcome for each order.

Two bindings · which order compiles

reads ↓ let price = 100 defined let total = price + tax all names in scope compiles · total = 120
values first · compiles · total = 120
// total first — rejected: price is used before it is defined
let total = price + tax
let price = 100
let tax = 20
// error FS0039: The value or constructor 'price' is not defined.

// values first — compiles
let price = 100
let tax = 20
let total = price + tax        // total = 120
Without JavaScript: the two orders and their outcomes
order compiler
total, then price, taxFS0039: 'price' is not defined
price, tax, then totalcompiles · total = 120
The same rule governs files. In the .fsproj, a <Compile Include="Types.fs" /> must come before a <Compile Include="Program.fs" /> that uses those types. The order is explicit and visible — a frequent first surprise for a C# developer, and, once learned, a reliable way to read a project's dependency direction straight off its file list.

F# reads a program the way you read a page: top to bottom, each name known by the time it is used. Order is not a style choice; it is the dependency graph, written down.

The C# habit

Declaration order is free; the compiler resolves names across the whole type and project.

F# today

Top-down, no forward references; a name precedes its use, and the .fsproj fixes file compile order.

Module complete

That is the whole move from classes to modules. Stateless helpers became a module of functions; an instance method became a function over a record that returns a copy; and the organisational rules — namespaces, access, members, and the significance of order — round out how F# code is arranged. The next module, C2.02, takes the other half of the C# mindset that needs translating: mutable state, and the move to immutability and expressions.

References

Sources

Related in this course