Option instead of null
A search that can fail answers `Some value` or `None`, and the type makes you deal with both. No null, no exception, no book that is not there.
A search that can fail
List.tryFind answers Some value when something matched and None when nothing did. That is a value of type option, and the type is the whole point: a function returning option cannot be used as if it returned a book, because the compiler will not let you reach a field on something that might be None.
There is no null in this course. A null is a value that pretends to be a book and fails later, in another function, with a message naming a line that is not the one at fault. None fails where you did not handle it, which is the line you are reading.
Working with an option
Option.map applies a function to the value inside a Some and passes a None straight through, which is how a chain of work over a search stays a chain. Option.defaultValue unpacks it with a fallback for the empty case. match handles the two cases with different behaviour, which is what you want when "not found" is something to say rather than a number to substitute.
Both of the last two are honest about the possibility: defaultValue says "zero when the book is missing", and match says so out loud. What neither does is forget that the search could fail.
Option.toList turns a Some into a one-element list and a None into the empty list, which is how an optional value joins a list without a branch. Option.count answers 1 or 0, and Option.exists asks whether a Some satisfies a predicate. These are small conveniences, but they keep a pipeline flat when the alternative is a match that only converts one shape into another.
From a list to an option, and out again
let pagesOf title =
books
|> List.tryFind (fun b -> b.Title = title)
|> Option.map (fun b -> b.Pages)
let pages = pagesOf "Dune" |> Option.defaultValue 0A hit, a miss and a doubled answer
Run it and read the last two lines: one title is on the shelf and one is not. Then add a third book to the list and ask for it.
%A prints a Some the way you would write it, but prints None as <null>, because that is how the runtime stores it. Take an option apart with match when it may be None; %A is for looking at values, not for writing reports.
An option is an ordinary union
F# does not build option into the compiler. It is a two-case union like any other: None carries nothing and Some carries one value. Some 3 has the type int option, and None can stand for an int option, a string option or any other missing value because it carries no type of its own.
That means match works on it with no special rules, and the compiler can tell when one of the two cases has been forgotten. The difference from a nullable value is not the syntax of the check; it is that the type says a check is needed before anything can be read.
Because option is data, the helpers that work on unions work here too. Option.map, Option.bind and Option.defaultValue are small functions over the two cases, not keywords with hidden behaviour. The type is built from the same cases you have already matched, so there is little new to learn once the union is familiar.
Map changes what is inside, bind joins two searches
Option.map applies a plain function to the value in a Some and passes None through. Option.bind is for a function that itself answers an option: it applies the function and returns the result directly instead of wrapping it in another Some. The difference is easiest to see when both searches can fail, because a miss at the first step means the second search never runs.
Option.defaultValue is the exit door. Use it at the edge of the program, where you can honestly say what a missing value should be replaced with, rather than in the middle of a lookup where it hides which step failed. Option.get is the door that throws, and this course does not use it. A default is a decision, and it reads best where the reader can see why it was chosen.
Option.orElse supplies a second option when the first is None, which is how a chain of lookups falls back from one source to another. When the fallback is expensive, Option.orElseWith takes a function and only calls it on a miss.
Option.get None throws at run time, so it undoes the safety the type was giving you. Using Option.map with a function that already returns an option leaves a nested Some (Some x); that is the case Option.bind exists for. And None is not null, so isNull is the wrong test.
Try these next
- Change
authorOfto useOption.mapinstead ofOption.bindand print the result, then explain the nesting. - Write
titleOfthat searches by author and returns the first matching title. - Replace
Option.defaultValuewith amatchthat prints a different sentence for a miss.
Two searches joined with bind
Run it and follow `Dune` from the book list to the author list to the country. Then add a book whose writer is not in the author list and ask for its country.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does List.tryFind answer?
