jonnifyfsharp

Dive C2.03.3

The interop boundary

Null and throws live at the edge. Convert there, and the core stays clean.

The two earlier dives describe an F# core with no null and no failure thrown for control flow. Real programs do not run in isolation: they call .NET libraries written in C#, and those return null for a missing value and throw when an operation cannot proceed. The discipline is a seam. At the edge where C# results cross into F#, convert each one — a null reference into an Option with Option.ofObj, a Nullable<T> with Option.ofNullable, a throwing call into a Result by catching at the boundary. Past that line, every value is an Option or a Result, and the rest of the code never touches a null or a try again.

C2.03.3 · part 1

A null reference becomes an Option

A C# method may return null for a reference type. FSharp.Core's Option.ofObj takes that value and returns None when it is null and Some v otherwise — the conversion is one call. For a value type wrapped in Nullable<T>, Option.ofNullable does the same. Both belong at the boundary, applied the moment the value arrives.

// 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

// a Nullable<int> -> Option, at the edge
System.Nullable<int>(5) |> Option.ofNullable   // val it : int option = Some 5
Option.ofObj
a nullable reference → Option: null becomes None, anything else Some.
Option.ofNullable
a System.Nullable<T> → Option, the same way for value types.
the rule
apply the conversion at the call site that crosses the boundary; never let a null travel deeper into F#.
C2.03.3 · part 2

A throwing call becomes a Result

For a call that throws, the boundary wraps it in a try and returns a Result: Ok for the value, Error for the caught failure. The with :? System.FormatException pattern catches one specific exception type — a deliberately narrow net, so a genuinely unexpected exception still propagates rather than being silently turned into an Error.

C# — the throwing API as the reader knows it

int n = int.Parse(s);   // returns 5 for "5"; throws FormatException for "x"

F# — caught at the seam, returned as a Result

let tryParseInt (s: string) =
    try Ok (System.Int32.Parse s)
    with :? System.FormatException -> Error "bad format"
// val tryParseInt : string -> Result<int,string>

tryParseInt "5"               // val it : Result<int,string> = Ok 5
tryParseInt "x"               // val it : Result<int,string> = Error "bad format"
The try sits at the seam, not scattered through the core. One wrapper converts the throwing call once; every function that uses tryParseInt downstream reads an Ok or an Error and never writes a try of its own.

Cross the seam

Three calls arrive from the C# side: one returns a value, one returns null, one throws. Pick one and watch it cross the boundary. The conversion at the seam turns each into a typed F# value — Some or None for the nullable lookup, Ok or Error for the throwing parse. Past the line, nothing downstream sees a null or a thrown exception.

the C# side · the seam · the F# core

C# side the seam · convert F# core int.Parse "5" try → Result at the boundary Ok 5
int.Parse "5" · tryParseInt "5" = Ok 5
Without JavaScript: the three calls, converted at the seam, by hand
the C# callthe seam converts withthe F# value
int.Parse "5"try → ResultOk 5
int.Parse "x" (throws)try → ResultError "bad format"
lookup → nullOption.ofObjNone

The seam is one line of code per crossing: a try wrapper, or an Option.ofObj. It is the only place in the program that knows null and throws exist.

C2.03.3 · part 3

The rule: convert once, at the edge

The module closes on one discipline. null and exceptions are facts of the .NET platform, and F# can produce and consume both when it has to. The choice is where: at the boundary, in a thin layer of conversions, and nowhere else. A function deep in the core takes an Option or a Result and returns one; it is never handed a null and never has to guard against a throw, because the seam already did.

The platform

.NET libraries return null and throw — facts F# inherits at every interop call.

The discipline

Convert at the seam — Option.ofObj · ofNullable · a try-wrap — and keep the core all Option and Result.

What this module covered

Three dives, two types, one boundary. Option retires null; Result retires the exception-as-control-flow; and the seam is where the C# that still does both is converted on the way in. The same two types run deeper through the language in chapter C1 — bind, the railway, and the computation expressions that thread them.

References

Sources

Related in this course