jonnifyfsharp

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.

C2.01.1 · part 1

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
What changed. The class wrapper became a 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 a static class played.
let square x = …
a function binding; public by default, no static needed 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(int) square x 25 val square : int -> int
F# · MathTools.square 5 = 25
Without JavaScript: the same three mappings, by hand
C# static method F# module function applied
MathTools.Square(5)MathTools.square 525
MathTools.Add(3, 4)MathTools.add 3 47
MathTools.Max(9, 4)MathTools.max 9 49

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.

C2.01.1 · part 2

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
The same move, one keyword shorter. Where C# distinguishes 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

Related in this course