option вместо null: безопасность по типам

F# заменяет коварный null типом option — отсутствие значения становится видимым в системе типов.

option — встроенное размеченное объединение с двумя вариантами: Some значение (значение есть) и None (значения нет).

Проблема null

В C#/Java null прячется в любой ссылке: можно забыть проверку и получить NullReferenceException в рантайме. F# в своём коде не использует null для собственных типов — отсутствие значения выражают явным option, и компилятор заставляет его обработать.

let findUser id =
    if id = 1 then Some "Алиса"
    else None

printfn "%A" (findUser 1)
printfn "%A" (findUser 99)

Вывод:

Some "Алиса"
None

Тип findUserint -> string option. Сигнатура честно говорит: результат может отсутствовать.

Разбор option

Достать значение можно только разобрав оба случая — забыть None не получится.

let greet id =
    match findUser id with
    | Some name -> sprintf "Привет, %s!" name
    | None -> "Пользователь не найден"

let findUser id = if id = 1 then Some "Алиса" else None
printfn "%s" (greet 1)
printfn "%s" (greet 99)

Вывод:

Привет, Алиса!
Пользователь не найден

Работа без распаковки: map и defaultValue

Часто значение преобразуют, не доставая его вручную. Option.map применяет функцию к содержимому Some, а к None ничего не делает. Option.defaultValue подставляет значение по умолчанию вместо None.

let result =
    Some 10
    |> Option.map (fun x -> x * 2)
    |> Option.defaultValue 0
printfn "%d" result

let empty =
    None
    |> Option.map (fun x -> x * 2)
    |> Option.defaultValue 0
printfn "%d" empty

Вывод:

20
0

Связывание вычислений: bind

Когда следующий шаг сам возвращает option, используют Option.bind — он «протягивает» None через всю цепочку, не вызывая последующие шаги.

let parsePositive (s: string) =
    match System.Int32.TryParse s with
    | true, n when n > 0 -> Some n
    | _ -> None

let doubled = parsePositive "21" |> Option.map (fun n -> n * 2)
printfn "%A" doubled

Вывод:

Some 42

Как работает под капотом

option — это обычный DU: type Option<'T> = None | Some of 'T. None представлен как пустой случай, Some хранит значение. Компилятор не позволяет обратиться к содержимому, не разобрав вариант, — отсюда безопасность. Функции map/bind/defaultValue — просто удобные обёртки над сопоставлением.

Частые ошибки

  • Грубо «вытаскивать» значение через .Value без проверки — на None это бросит исключение.
  • Возвращать null из F#-кода вместо None — теряется типобезопасность.
  • Путать Option.map (функция возвращает обычное значение) и Option.bind (функция возвращает option).

Итоги

  • option заменяет null: Some x — есть значение, None — нет.
  • Сигнатура с option честно сообщает о возможном отсутствии значения.
  • Компилятор требует обработать оба случая — забыть None нельзя.
  • map, defaultValue, bind позволяют работать с option без ручной распаковки.
Проверьте себя
1. Какие варианты у типа option?
Atrue и false
BSome значение и None
Cnull и значение
DOk и Error
2. Почему option безопаснее null?
AОн быстрее
BОтсутствие значения видно в типе, и компилятор требует его обработать
CОн занимает меньше памяти
DОн всегда не пустой
3. Что делает Option.defaultValue 0 для None?
AБросает исключение
BВозвращает 0 вместо None
CВозвращает None
DВозвращает Some 0