Monady
Monada to pewien obiekt matematyczny z określonymi operacjami, który stosowany jest w programowaniu funkcyjnym w kilku różnych celach, w zależności od tego jak ta monada jest określona.
Pierwszy moment, kiedy spotykamy się z monadą w czystym języku programowania (np. Haskell) to kiedy mamy do czynienia z kontaktem ze światem. W Haskellu jedyny sposób aby otworzyć plik, zapisać coś na konsoli, itp. to użyć wbudowanej monady IO
.
W F# takiej potrzeby nie ma, więc zaczniemy od monad, które ułatwiają życie. Pierwszą z nich będzie monada Option
.
Jeśli pamiętamy to typ Option
był taki:
1:
|
|
Podczas jego używania będziemy bardzo często spotykać się takim wzorcem:
1: 2: 3: 4: |
|
Z jednej strony mamy funkcję Option.map
, ale jej użycie może nie zawsze być wygodne w porównaniu do monady Option
, szczególnie jeśli połączymy ją z wyrażeniami komputacyjnymi F#.
Ale po kolei.
Monada w najprostszej postaci to pewien generyczny typ danych z jednym argumentem oraz dwie funkcje: bind
i return
. Zadaniem return
jest opakować pewną wartość w daną monadę. Zadaniem bind
jest umożliwić operacje na opakowanej wartości, w środowisku monady.
Najlepiej jest zrozumieć to na przykładzie.
1: 2: 3: 4: 5: 6: 7: 8: |
|
Tak wygląda implementacja funkcji return
oraz bind
dla monady Option
. Dorzuciłem jeszcze fail
, żeby zaraz pokazać jak tego będziemy używać.
1: 2: 3: 4: 5: 6: |
|
Ale prawdziwa moc pokaże się dopiero za chwilę. Bo dla jednej operacji oczywiście nie opłaca się tworzyć skomplikowanego wzorca. Nie przejmując się wcześniejszym możliwym błędem dodajemy 5 do wyniku.
1: 2: 3: 4: |
|
Więc jeśli jest sukces to idziemy ścieżką sukcesu. Jeśli nie to propagujemy None
.
A teraz wspomniane wyrażenia komputacyjne. To jest jedyny moment gdzie podczas tego warsztatu dotkniemy klas, bo jest to interfejs wykorzystywany przez kompilator F#.
1: 2: 3: 4: 5: |
|
Zdefiniowaliśmy pewnien typ i utworzyliśmy jego instancję. Pozwala nam to na coś takiego:
1: 2: 3: 4: 5: 6: |
|
Dostajemy tę super składnię to pracy z monadami. return
jest oczywiste, a let!
to w rzeczywistości bind
. Wszystkie operacje są wykonywane w środowisku monady Option
i jeśli w dowolnym momencie wystąpi błąd to na sam koniec dostaniemy None
, ale nie musimy sie tym martwić i wielkrotnie sprawdzać.
Pozbyliśmy się wartości null
, zastępując ją wartością None
, a teraz nie musimy jej nawet sprawdzać.
Zadanie
Napisz monadę na typie:
1:
|
|
Ta monada jest ciut lepsza od monady Option
w tym, że pozwala nam dodać informację co poszło nie tak, a nie tylko, że coś poszło nie tak.
Kompilator może mieć wąty kiedy używamy tylko jednego z typów, więc trzeba mu wtedy jawnie dodać typ deklarowanej wartości np:
1:
|
|
Ale zazwyczaj będzie się właściwie domyślał.
Pytania? Jeśli wszystko jasne, to przechodzimy do następnego modułu
module Option
from Microsoft.FSharp.Core
--------------------
type Option<'a> =
| None
| Some of 'a
Full name: Option_monad.Option<_>
Full name: Option_monad.doSomething
Full name: Option_monad.func
Full name: Option_monad.optReturn
Full name: Option_monad.optFail
Full name: Option_monad.optBind
Full name: Option_monad.optDivide
Full name: Option_monad.test1
Full name: Option_monad.test2
Full name: Option_monad.optAdd5
Full name: Option_monad.test3
Full name: Option_monad.test4
type OptionBuilder =
new : unit -> OptionBuilder
member Bind : m:Option<'a> * f:('a -> Option<'b>) -> Option<'b>
member Return : x:'c -> Option<'c>
Full name: Option_monad.OptionBuilder
--------------------
new : unit -> OptionBuilder
Full name: Option_monad.OptionBuilder.Return
Full name: Option_monad.OptionBuilder.Bind
val option : OptionBuilder
Full name: Option_monad.option
--------------------
type 'T option = Option<'T>
Full name: Microsoft.FSharp.Core.option<_>
Full name: Option_monad.test5
| Success of 'TSuccess
| Failure of 'TError
Full name: Option_monad.Result<_,_>
union case Result.Failure: 'TError -> Result<'TSuccess,'TError>
--------------------
active recognizer Failure: exn -> string option
Full name: Microsoft.FSharp.Core.Operators.( |Failure|_| )
Full name: Option_monad.succ10
val int : value:'T -> int (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.int
--------------------
type int = int32
Full name: Microsoft.FSharp.Core.int
--------------------
type int<'Measure> = int
Full name: Microsoft.FSharp.Core.int<_>