Ну как можно не посветить монадам первый пост в новом году?

В этой серии постов я хочу просто собрать в кучу определения на F# таких эпических монад, как maybe, state, continuation и, возможно, некоторых других. Сомневаюсь, что кому-то они реально понадобятся в повседневной разработке на F# (когда есть изменяемые let-привязки вместо монады state и встроенные в язык seq { … }, [ … ] и [| … |], а так же async { … } из состава стандартной библиотеки), поэтому серия носит чисто академический характер (например, если вам хочется в самых известных монадах, но при этом не хочется курить детали type classes или разбирать синтаксис Haskell). Я не буду приводить реализации всех возможных методов классов-builder’ов computation expression (перечисление которых и правила трансформации можно найти здесь), а лишь тот набор, который позволят понять суть монады и пример её использования.

Начнём с монады maybe, а в качестве типа вычисления M<’a> будем использовать тип Option<’a> из стандартной библиотеки F#. Сигнатура:

namespace FSharp.Monads

type MaybeBuilder =
  new: unit -> MaybeBuilder
  member Zero: unit -> 'a option
  member Bind: 'a option * ('a -> 'b option) -> 'b option
  member Return: 'a -> 'a option
  member ReturnFrom: 'a option -> 'a option

Реализация:

namespace FSharp.Monads

type MaybeBuilder() =
  member b.Zero() = None
  member b.Bind(x, f) =
    match x with Some x -> f x
               | None   -> None
  member b.Return x = Some x
  member b.ReturnFrom x = x : _ option

В качестве пример использования, можно привести программу, которая ожидает ввод пользователем целого числа и пытается найти порядковый номер введённого числа в последовательности простых чисел, при этом в случае не числового ввода или ввода числа, не являющегося простым, программа останавливается и возвращает None:

open System
open FSharp.Monads

let maybe = MaybeBuilder()

/// Список простых чисел от 2 до 100
let primes =
  let is_prime x = // неэффективно, лишь для примера
    Seq.forall (fun y -> x % y > 0) { 2 .. x/2 }
  { 2 .. 100 } |> Seq.filter is_prime
               |> Seq.toList

/// Попытка считывания с консоли целого числа
let inputInt32() =
  maybe {
   let str = Console.ReadLine()
   let success, value = Int32.TryParse str
   if success then return value
  }

/// Попытка считывания с консоли простого числа
let tryInputPrime() =
  maybe {
    printfn "введите простое число от 2 до 100:"
    let! prime = inputInt32()
    let! index = List.tryFindIndex ((=) prime) primes
    return prime, index + 1
  }

match tryInputPrime() with
| Some(prime, index) ->
          printfn "ввели простое число %d (№%d)" prime index
| None -> printfn "ввод простого числа завершился неудачей"

Вот как выглядит функция inputInt32 “без сахара”, обратите внимание на вызов метода Zero():

/// Попытка считывания с консоли целого числа
let inputInt32'() =
  let str = Console.ReadLine()
  let success, value = Int32.TryParse str
  if success
    then maybe.Return(value)
    else maybe.Zero()

А вот и вся “поднаготная” функции tryInputPrime:

/// Попытка считывания с консоли простого числа
let tryInputPrime'() =
  printfn "введите простое число от 2 до 100:"
  maybe.Bind(
    inputInt32(),
    fun prime ->
      maybe.Bind(
        List.tryFindIndex ((=) prime) primes,
        fun index ->
          maybe.Return(prime, index + 1)))

Обратите так же внимание на модуль Option из состава стандартной библиотеки F#, он содержит дополнительные функции для работы со значениями типа 'a option, такие как map и fold и другие.