Dive C2.01.1
Static classes → modules
The most direct translation: a bag of stateless helpers, with the class taken away.
A C# static class exists to hold functions that carry no state —
Math, a StringHelpers, a project's utility class. It is a
container, nothing more: the class never has an instance. F# has a keyword for exactly that container —
module — and inside it, each helper is a plain let-bound
function. There is no public static to write on each one, and where a C# call reads
MathTools.Square(5), the F# call reads MathTools.square 5.
This dive makes the substitution, then shows the two ways to reach a module's functions: qualified by the module
name, or brought in unqualified with open.
The container becomes a keyword
In C#, the static class is the smallest unit that can hold a free function: the
language has no top-level functions, so even Square must live on a type. Each method
is marked public static, and the class is referenced by name, never instantiated.
F# replaces the whole arrangement with module. The functions inside are bound with
let — the same keyword used for any value — and their accessibility is
public by default. The two forms below are equivalent: the same three helpers, called the same way.
C#
static class MathTools { public static int Square(int x) => x * x; public static int Add(int a, int b) => a + b; public static int Max(int a, int b) => a > b ? a : b; } // MathTools.Square(5) -> 25
F#
module MathTools = let square x = x * x let add a b = a + b let max a b = if a > b then a else b MathTools.square 5 // val it : int = 25 MathTools.add 3 4 // val it : int = 7
module, and
public static int Square(int x) => … became let square x = ….
F# infers the type int -> int from the multiplication; nothing is annotated, and
nothing is marked public.- module
- the unit that holds
let-bound functions and values — the role astatic classplayed. - let square x = …
- a function binding; public by default, no
staticneeded because a module is never instantiated. - MathTools.square 5
- a qualified call: arguments are juxtaposed, not parenthesised and comma-separated.
- val square : int -> int
- the inferred signature FSI prints — the type read from the body.
Map a C# helper to its module function
Pick one of the three helpers. The diagram shows the C# static method on the left and the F# module function
it becomes on the right, and the readout runs the F# form over a fixed input — the value is the one
dotnet fsi prints.
A static method · the same, as a module function
| C# static method | F# module function | applied |
|---|---|---|
| MathTools.Square(5) | MathTools.square 5 | 25 |
| MathTools.Add(3, 4) | MathTools.add 3 4 | 7 |
| MathTools.Max(9, 4) | MathTools.max 9 4 | 9 |
Same helpers, same call shape, one fewer concept. The class was scaffolding for the function; the module is the function's home without the scaffolding.
open: the C# using static
A qualified call — MathTools.square — names the module every time. To drop
the qualifier, C# offers using static MathTools;, after which Square
is in scope unqualified. F#'s equivalent is open: it brings a module's names into
scope so they can be called bare.
C#
using static MathTools; // now unqualified: var y = Square(5); // 25
F#
open MathTools // now unqualified: square 5 // val it : int = 25
using
(a namespace) from using static (a type's members), F# uses one keyword,
open, for both a namespace and a module — because a module already is the
container. The next dive in this module covers how to make a module's names available without any
open at all.The C# habit
A free function needs a static class to live on, each method marked public static.
F# today
A module of let-bound functions, public by default, reached by name or via open.
Where this leads
Stateless helpers were the easy case: no instance, no state, nothing to carry. The harder and more revealing
case is an instance method — one that reads and writes an object's fields. That is where the
this a C# method receives implicitly becomes an ordinary argument, and where a
"mutation" turns into a returned copy. Dive C2.01.2 makes that move with an account record.
References
Sources
- Microsoft — F# modules — declaring a
moduleand itsletbindings. - Microsoft — the
openkeyword — bringing module and namespace names into scope. - The F# Language Specification — module definitions and accessibility.
Related in this course
- C2.01.2 — Instance methods → functions — the case with state to carry.
- C2.01 — Classes → modules & functions — the module this dive opens.
- C1.03 — Functions — the
let-bound functions a module collects.