fun-girlz


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: 
type Option<'a> = None | Some of 'a

Podczas jego używania będziemy bardzo często spotykać się takim wzorcem:

1: 
2: 
3: 
4: 
let func opt =
    match opt with
    | None -> None
    | Some x -> doSomething x

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: 
let optReturn x = Some x
let optFail = None

//optBind : Option<'a> -> ('a -> Option<'b>) -> Option<'b>
let optBind opt f =
    match opt with
    | None -> None
    | Some x -> f x

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: 
let optDivide (a,b) =
    if b = 0 then optFail
    else optReturn (a / b)

let test1 = optBind (optReturn (4,2)) optDivide
let test2 = optBind (optReturn (4,0)) optDivide

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: 
let optAdd5 x = optReturn (x + 5)

let test3 = optBind test1 optAdd5
let test4 = optBind test2 optAdd5

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: 
type OptionBuilder() =
    member this.Return(x) = optReturn x
    member this.Bind(m, f) = optBind m f

let option = OptionBuilder()

Zdefiniowaliśmy pewnien typ i utworzyliśmy jego instancję. Pozwala nam to na coś takiego:

1: 
2: 
3: 
4: 
5: 
6: 
let test5 b = 
    option {
        let! d = optDivide (7, b)
        let! e = optAdd5 d
        return e
    }

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: 
type Result<'TSuccess, 'TError> = Success of 'TSuccess | Failure of 'TError

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: 
let succ10 : Result<int, 'e> = Success 10

Ale zazwyczaj będzie się właściwie domyślał.

Pytania? Jeśli wszystko jasne, to przechodzimy do następnego modułu

Multiple items
module Option

from Microsoft.FSharp.Core

--------------------
type Option<'a> =
  | None
  | Some of 'a

Full name: Option_monad.Option<_>
union case Option.None: Option<'a>
union case Option.Some: 'a -> Option<'a>
val doSomething : 'a -> Option<'b>

Full name: Option_monad.doSomething
val func : opt:Option<'a> -> Option<'b>

Full name: Option_monad.func
val opt : Option<'a>
val x : 'a
val optReturn : x:'a -> Option<'a>

Full name: Option_monad.optReturn
val optFail : Option<'a>

Full name: Option_monad.optFail
val optBind : opt:Option<'a> -> f:('a -> Option<'b>) -> Option<'b>

Full name: Option_monad.optBind
val f : ('a -> Option<'b>)
val optDivide : a:int * b:int -> Option<int>

Full name: Option_monad.optDivide
val a : int
val b : int
val test1 : Option<int>

Full name: Option_monad.test1
val test2 : Option<int>

Full name: Option_monad.test2
val optAdd5 : x:int -> Option<int>

Full name: Option_monad.optAdd5
val x : int
val test3 : Option<int>

Full name: Option_monad.test3
val test4 : Option<int>

Full name: Option_monad.test4
Multiple items
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
val this : OptionBuilder
member OptionBuilder.Return : x:'c -> Option<'c>

Full name: Option_monad.OptionBuilder.Return
val x : 'c
member OptionBuilder.Bind : m:Option<'a> * f:('a -> Option<'b>) -> Option<'b>

Full name: Option_monad.OptionBuilder.Bind
val m : Option<'a>
Multiple items
val option : OptionBuilder

Full name: Option_monad.option

--------------------
type 'T option = Option<'T>

Full name: Microsoft.FSharp.Core.option<_>
val test5 : b:int -> Option<int>

Full name: Option_monad.test5
val d : int
val e : int
type Result<'TSuccess,'TError> =
  | Success of 'TSuccess
  | Failure of 'TError

Full name: Option_monad.Result<_,_>
union case Result.Success: 'TSuccess -> Result<'TSuccess,'TError>
Multiple items
union case Result.Failure: 'TError -> Result<'TSuccess,'TError>

--------------------
active recognizer Failure: exn -> string option

Full name: Microsoft.FSharp.Core.Operators.( |Failure|_| )
val succ10 : Result<int,'e>

Full name: Option_monad.succ10
Multiple items
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<_>
F# Project