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.
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
privatea C# dev expects. - [<AutoOpen>]
- makes a module's names available without an
open.
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
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.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
// 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
| order | compiler |
|---|---|
| total, then price, tax | FS0039: 'price' is not defined |
| price, tax, then total | compiles · total = 120 |
.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
- Microsoft — F# namespaces — namespaces, and how they differ from modules.
- Microsoft — F# members — type members, including instance properties.
- Microsoft — F# compiler options & the source file order — why
.fsprojfile order is significant.
Related in this course
- C2.01.2 — Instance methods → functions — the record this dive adds members to.
- C2.01 — Classes → modules & functions — the module this dive closes.
- C2 — F# for C# developers — the chapter, and the modules that follow.