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.
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
Nonefornull,Some vfor 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 ofNoneby accident.
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
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.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 | converter | 'T option |
|---|---|---|
| "hi" (string) | Option.ofObj | Some "hi" |
| null (string) | Option.ofObj | None |
| Nullable<int>(5) | Option.ofNullable | Some 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
- FSharp.Core API reference —
Option.ofObjandOption.ofNullable, the seam converters used here. - Microsoft — F# documentation — null handling in F#, and
[<AllowNullLiteral>]. - Microsoft — .NET documentation —
Nullable<T>, nullable reference types, andNullReferenceException.
Related in this course
- C2.03 — From null to Option, from exceptions to Result — the interop-boundary dive this extends.
- C2.08.1 — Calling C# / the BCL from F# — the tuple that the parse-seam produces.
- C1.08 — Option, Result & railway-oriented errors —
Optionin language depth.