Dive C1.08.2
Result
The same two-case shape as Option — but the failing side carries a reason.
Where Option says whether a value is present,
Result<'T,'TError> says whether an operation succeeded and, when it did not,
why. It has two cases: Ok x holds a successful value of type
'T; Error e holds a failure value of type
'TError — commonly a string message, but it can be any
type. The transforming functions mirror Option's: Result.map
changes the Ok, Result.mapError changes the
Error, and Result.bind chains a step that can itself fail.
This dive covers all three, and one real wrinkle of the F# type system that surfaces the first time you map a
bare Ok at the REPL.
Ok of 'T, or Error of 'TError
Result is a union with two type parameters: the type of a success and the type of a
failure. Its declaration in FSharp.Core is the two constructors below. A function that can fail with a
described error returns one: the success path carries the value, the failure path carries the reason.
type Result<'T, 'TError> = | Ok of 'T | Error of 'TError
A string-to-int parser is the canonical example. .NET's Int32.TryParse returns a
tuple of a success flag and the parsed value; F# matches on it and turns the two cases into
Ok or Error with a message. The signature
string -> Result<int,string> tells the caller both how it succeeds and how it
fails.
let parse (s: string) = match System.Int32.TryParse s with | true, v -> Ok v | false, _ -> Error (sprintf "not an int: %s" s) // val parse : string -> Result<int,string> parse "42" // val it : Result<int,string> = Ok 42 parse "x" // val it : Result<int,string> = Error "not an int: x"
- Result<'T,'TError>
- a union:
Ok of 'Ton success,Error of 'TErroron failure. - 'TError
- the failure type — often a
stringmessage, but free to be a union of error cases. - vs. Option
Optionrecords that a value is missing;Resultrecords why a step failed.- vs. exceptions
- the failure is a returned value the caller must handle, not a thrown one it might forget to catch.
Explore parse s
Pick an input string. parse sends it down one of two rails: the
Ok rail when the string is an integer, carrying the parsed
value; the Error rail otherwise, carrying the exact message the
function built. The readout shows the precise FSI value, message and all.
parse s · an Ok rail and an Error rail
| input s | parse s |
|---|---|
| "42" | Ok 42 |
| "-3" | Ok -3 |
| "x" | Error "not an int: x" |
The two rails are not exceptional flow and normal flow — they are two values of one type.
Whichever rail a call lands on, the caller reads it the same way: with a match on
Ok and Error.
map, mapError, bind
Three functions cover the work. Result.map transforms the value inside an
Ok and leaves an Error untouched.
Result.mapError does the mirror image: it rewrites the
Error and leaves an Ok alone — useful for wrapping a
low-level message in a friendlier one. Result.bind chains a step that itself returns
a Result, which is the engine of the next dive.
// map: transform the Ok, leave an Error alone ((Ok 5): Result<int,string>) |> Result.map (fun x -> x * 2) // val it : Result<int,string> = Ok 10 // mapError: rewrite the Error, leave an Ok alone ((Error "boom"): Result<int,string>) |> Result.mapError (fun e -> "wrapped: " + e) // val it : Result<int,string> = Error "wrapped: boom" // bind: chain a step that itself returns a Result ((Ok 7): Result<int,string>) |> Result.bind (fun n -> if n > 0 then Ok (n * 10) else Error "neg") // val it : Result<int,string> = Ok 70
One real wrinkle: the value restriction
The annotations : Result<int,string> in the previous block are deliberate.
Written without them, the very first line is rejected by the compiler — and the reason is worth knowing,
because it is a genuine corner of the type system, not a quirk of this example.
When Ok 5 is piped into Result.map at the top level of an
FSI session, F# infers the success type as int but has nothing to pin the error
type, so it would be a generic Result<int,'a>. The
value restriction forbids a generic value at the top level — a value is read the same
everywhere, and an open type parameter could break that — so the compiler reports
FS0030.
// rejected: the error type is unconstrained at the top level Ok 5 |> Result.map (fun x -> x * 2) // error FS0030: Value restriction. The value 'it' has an inferred generic type // fixed: annotate the Result, pinning both type parameters ((Ok 5): Result<int,string>) |> Result.map (fun x -> x * 2) // val it : Result<int,string> = Ok 10
Ok or Error with no surrounding context. Inside a
function whose return type is known — like parse above, annotated
string -> Result<int,string> — both parameters are already pinned, and no
annotation is needed. The annotation is a fact about the REPL session, not about
Result.The idea
Failure is a value with a reason attached — read on the same footing as success.
F# today
Ok / Error, threaded with map · mapError · bind; the error type pinned by context.
Where this leads
Result.bind is the connector that makes a sequence of failable steps into one
computation. When several steps each return a Result and the first
Error should stop the rest, the shape has a name — the
railway — and dive C1.08.3 builds one.
References
Sources
- Microsoft — F# documentation — results, error handling, and the value restriction.
- FSharp.Core API reference — the
Resultmodule:map,mapError,bind. - The F# Language Specification — the value restriction and generalisation rules.
Related in this course
- C1.08.3 — Railway-oriented errors — chaining results with
bind. - C1.08.1 — Option — the same shape without a reason.
- Elixir — Purity — the BEAM tuple
{:error, reason}, returned rather than raised.