W programowaniu funkcyjnym pracuje się czesto znacznie bliżej matematyki, a co za tym idzie pewne pojęcia brzmią bardzo skomplikowanie choć wcale takie być nie muszą. Dziś zobaczymy czym jest monada oraz jak F# ułatwia nam korzystanie z monad przez wyrażenia komputacyjne.
Monada to monoid z kategorii endofunktorów
Tak brzmi matematyczne określenie monady. Chociaż jak zacząłem się wczytywać w teorię kategorii, z której to określenie pochodzi, to przestałem rozumieć czym monady właściwie są.
Ale w skrócie:
- endofunktor ~ funkcja
'a -> 'a - monoid - trójka (S, e, 𐌈) gdzie
𐌈: S * S -> Soraz∀(a in S) e𐌈a = a𐌈e = aoraz zachodzi łączność.
Jak to się ma do monady? Ponieważ funkcje 'a -> 'a można ze sobą składać (𐌈) i mają identyczność fun x -> x to dostajemy prawie monoid. Jedyne czego nam brakuje to łączność, więc musimy ograniczyć nasze funkcje do takich gdzie kolejność ich aplikacji nie ma znaczenia. I tadaa - mamy monoid, który nazwiemy monadą.
Powrót do rzeczywistości
Ale tak naprawdę, z praktycznego punktu widzenia, monada jest trochę prostsza do zrozumienia. Zaprezentuję to na przykładzie:
type Error =
| InvalidOperation
| ParsingError
| Exception of System.Exception
type Result<'a> =
| Success of 'a
| Failure of Error
Naszym typem, który będzie określał wartość monadyczną jest Result<'a>. Określa on sukces wraz z pewną wartością oraz porażkę. Aby była to monada to potrzebujemy jeszcze dwóch operacji bind oraz return.
Operacja return ma sygnaturę 'a -> Result<'a> i jedynym jej zadaniem jest opakowanie naszej wartości w kontener jakim jest Result<'a>.
Operacja bind natomiast jest operacją łączącą wartość monadyczną z przekształceniem f: 'a -> Result<'b>.
Może czas na przykład implementacji, żeby lepiej to wchłonąć:
let ``return`` x = Success x
let bind result f =
match result with
| Success x -> f x
| Failure err -> Failure err
Notka: w F# return jest słowem kluczowym, ale zawierając nazwę w `` możemy używać w środku czegokolwiek.
O ile return jest jasny, to popatrzmy na bind. Jeśli nasz result jest sukcesem to wyciągamy z niego wartość i aplikujemy funkcję f, która zwraca nam Result<'b>. Ogólnie 'a może się równać 'b w typie funkcji f (wyżej). A jeśli result był porażką, to nie bawimy się w aplikowanie funkcji tylko zwracamy tą porażkę.
Taki schemat działania pozwala nam na dość sensowne kontrolowanie błędów w naszej aplikacji. Zamiast rzucać wyjątkiem, zwracamy porażkę, która niesie informacje o błędzie.
W dodatku możemy właśnie łączyć kilka funkcji w łańcuch i jeśli w której kolwiek dojdzie do błędu to na niej się ten łańcuch zatrzyma. To daje nam bind.

Railway Oriented Programming
from F# for fun and profit
Za pomocą właśnie takich kontrukcji można uzyskać programowanie zorientowane na kolej. Mamy dwa tory: tor sukcesu i tor porażki. Jeśli na torze sukcesu dojdzie do błędu to zjeżdżamy na tor porażki i już z niego nie wracamy.
Przykład funkcji używającej tego schematu:
let myProcess state =
validate state
>=> update
>=> send
Gdzie operator >=> to operacja bind.
Więcej na ten temat znajdziecie tu: Railway Oriented Programming.
Wyrażenia komputacyjne
W F# aby uprościć używanie monad, zostało dodane coś takiego jak Computation Expressions. W podlinkowanym artykule znajdziecie pełen zestaw funkcji jakie można wykorzystać, jednak ja przedstawię tylko te które są ważne dla naszego przykładu.
Wyrażenia komputacyjne możecie kojarzyć chociażby z monady Async<'T>, która dostarcza operacje asynchroniczne w F#. Wyrażenie komputacyjne umieszczamy wtedy w bloku async { }. Twórcy F# udostępniają nam interfejs do tworzenia naszych własnych wyrażeń.
Zaczniemy od definicji klasy ResultBuilder
type ResultBuilder() =
member this.Return(x) = ``return`` x
member this.ReturnFrom(result) = result
member this.Bind(x, f) = bind x f
Ta klasa jest implementacją interfejsu wyrażenia komputacyjnego. Kolejnym naszym krokiem jest utworzenie jej instancji:
let result = ResultBuilder()
Możemy teraz używać bloków result { }, w taki sposób, że:
let a = result { return 1 } // val a : Result<int> = Success 1
let b : Result<int> =
result { return! Failure InvalidOperation }
// val b : Result<int> = Failure InvalidOperation
let c r = result {
let! v = r
if v = 1 then return 2
else return 3
}
// c a = Success 2
// c b = Failure InvalidOperation
// c (c a) = Success 3
// c (c b) = Failure InvalidOperation
Ogólnie wygląda to tak:
{ return x }-result.Return(x){ return! x }-result.ReturnFrom(x){ let! y = x in expr }-result.Bind(x, fun y -> expr)
Jaki jest cel tych wyrażeń komputacyjnych? Przede wszystkim uproszczenie czytelności kodu. Takie bloki czyta się łatwiej niż szeregi zagnieżdżonych funkcji.
