jonnifyfsharp

Dive C2.08.3

The null & Nullable boundary

C# returns null and Nullable. F# has neither. Lift both at the edge, once.

A C# library returns null for a missing reference and Nullable<T> for a missing value type. The F# core has neither, by design — and the rule for keeping it that way is a seam. At the line where a C# result crosses into F#, lift it into an Option: Option.ofObj turns a reference into Some v or None, and Option.ofNullable does the same for a Nullable<T>. Convert once, at the edge, and nothing past the line is ever null. The dereference that throws a NullReferenceException has nowhere left to happen.

C2.08.3 · part 1

A null reference becomes an Option

Option.ofObj takes a nullable reference and returns None when it is null and Some v otherwise. It belongs exactly at the boundary, applied the moment the value arrives from C#. C# guards a null with ?. at every use; F# guards it once, at the edge, and from there the value is an Option the compiler tracks.

C# — a method that may return null, guarded at each use

string Lookup(int id);            // may return null
var len = Lookup(9)?.Length ?? 0;   // the ?. guard repeats wherever the value is used

F# — lift to Option once, at the seam

// a reference that might be null -> Option, at the edge
Option.ofObj "hi"             // val it : string option = Some "hi"
Option.ofObj (null: string)   // val it : string option = None
// past the seam the value is a string option — no further null guards
Option.ofObj
takes a nullable reference; returns None for null, Some v for a value. The reference-type seam converter.
convert once
applied at the boundary, not at each use — one guard replaces C#'s repeated ?..
past the seam
the value's type is 'T option; the F# core has no operation that pulls a value out of None by accident.
C2.08.3 · part 2

A Nullable<T> becomes an Option

Value types are not references, so they use a different C# stand-in for "missing" — Nullable<T>, written int? in C#. Option.ofNullable is its seam converter: it returns Some v when the Nullable has a value and None when it does not. Same discipline, same place — the boundary.

C# — a Nullable value type

int? maybe = GetCount();      // Nullable<int> — has a value, or does not

F# — lift the Nullable to Option at the seam

// a Nullable<int> -> Option, at the edge
System.Nullable<int>(5) |> Option.ofNullable   // val it : int option = Some 5
System.Nullable<int>()  |> Option.ofNullable   // val it : int option = None
The two converters split by kind: Option.ofObj for a reference type that may be null, Option.ofNullable for a value type wrapped in Nullable<T>. Both land an 'T option in the core. This is the same boundary the C2.03 interop-boundary dive describes, where a throwing call is wrapped into a Result by the same once-at-the-edge rule.
C2.08.3 · part 3

The rule: contain the mistake at the edge

null is a fact of the .NET platform, and F# can produce and consume it when it must — a type can even carry [<AllowNullLiteral>] to accept a C# null when an interface contract requires it. The choice is where null is allowed to live: at the boundary, in a thin layer of conversions, and nowhere else. The function that throws on a null dereference is the most expensive default in software; the seam removes the default from the whole core and keeps it at one wall.

The C# value

A reference that may be null, or a Nullable<T> value type that may be empty — guarded by convention at each use.

The F# form

Lifted at the seam — Option.ofObj for a reference, Option.ofNullable for a Nullable — to an 'T option the core reads with a total match.

Cross the seam

Pick a C#-style result. Each one crosses the boundary and lands as an Option: a non-null reference lifts to Some, a null lifts to None, and a populated Nullable lifts to Some. The readout shows the converter used and the exact FSI value past the seam. The dashed line is the boundary — to its right, nothing is ever null.

A C# result · lifted to Option at the boundary

C# result at the seam 'T option boundary "hi" Option.ofObj Some · / · None Some "hi" None
"hi" · Option.ofObj "hi" = Some "hi" · past the seam, a string option
Without JavaScript: the same three results, lifted by hand
C# resultconverter'T option
"hi" (string)Option.ofObjSome "hi"
null (string)Option.ofObjNone
Nullable<int>(5)Option.ofNullableSome 5

Three C# results, one wall, one type past it. The NullReferenceException is not handled in the core — it is made unreachable, because the seam already lifted every null into a None.

Where this closes

The three dives crossed the seam in both directions and contained the one type that does not cross cleanly. F# calls the BCL, with the out parameter returning as a tuple; F# types present predictable shapes to C#; and null and Nullable<T> lift to Option at the edge. The interop facts stay at the boundary, and the F# core stays clean — the rule this module opened on.

References

Sources

Related in this course