サーチ…
モナドを理解することは練習から来る
このトピックは、中級から上級のF#開発者を対象としています
"モナドとは何ですか?"よくある質問です。これは簡単に答えることができますが、銀河へのヒッチハイカーのガイドのように、私たちは私たちが何を求めているのか分からなかったので、私たちは答えを理解できません。
モナドを理解する方法は、モナドを練習することだと多くの人が信じています。プログラマーとして、我々は通常、Liskovの置換原理、サブタイプまたはサブクラスの数学的基礎を気にしません。これらのアイデアを使用することによって、我々は彼らが表現しているものに対する直感を得ることができました。 Monadsの場合も同様です。
Monadsを使い始めるために、この例はMonadic Parser Combinatorライブラリを構築する方法を示しています。これはあなたが始めるのに役立つかもしれませんが、あなた自身のMonadicライブラリを書くことから理解が得られます。
十分な散文、コードの時間
パーサータイプ:
// A Parser<'T> is a function that takes a string and position
// and returns an optionally parsed value and a position
// A parsed value means the position points to the character following the parsed value
// No parsed value indicates a parse failure at the position
type Parser<'T> = Parser of (string*int -> 'T option*int)
このパーサの定義を使用して、いくつかの基本的なパーサ関数を定義します
// Runs a parser 't' on the input string 's'
let run t s =
let (Parser tps) = t
tps (s, 0)
// Different ways to create parser result
let succeedWith v p = Some v, p
let failAt p = None , p
// The 'satisfy' parser succeeds if the character at the current position
// passes the 'sat' function
let satisfy sat : Parser<char> = Parser <| fun (s, p) ->
if p < s.Length && sat s.[p] then succeedWith s.[p] (p + 1)
else failAt p
// 'eos' succeeds if the position is beyond the last character.
// Useful when testing if the parser have consumed the full string
let eos : Parser<unit> = Parser <| fun (s, p) ->
if p < s.Length then failAt p
else succeedWith () p
let anyChar = satisfy (fun _ -> true)
let char ch = satisfy ((=) ch)
let digit = satisfy System.Char.IsDigit
let letter = satisfy System.Char.IsLetter
satisfy
与えられた関数であるsat
機能は、我々が経過していない場合は、成功したパーサ生成EOS
、現在の位置の文字が経過sat
機能を。使用してsatisfy
私たちは便利な文字パーサの数を作成します。
これをFSIで実行する:
> run digit "";;
val it : char option * int = (null, 0)
> run digit "123";;
val it : char option * int = (Some '1', 1)
> run digit "hello";;
val it : char option * int = (null, 0)
私たちにはいくつかの基本的なパーサーがあります。パーサーコンビネータ関数を使用して、それらをより強力なパーサに結合します
// 'fail' is a parser that always fails
let fail<'T> = Parser <| fun (s, p) -> failAt p
// 'return_' is a parser that always succeed with value 'v'
let return_ v = Parser <| fun (s, p) -> succeedWith v p
// 'bind' let us combine two parser into a more complex parser
let bind t uf = Parser <| fun (s, p) ->
let (Parser tps) = t
let tov, tp = tps (s, p)
match tov with
| None -> None, tp
| Some tv ->
let u = uf tv
let (Parser ups) = u
ups (s, tp)
名前と署名は恣意的に選択されていませんが、これについては掘り下げません。代わりに、パーサーをより複雑なものにbind
するためにbind
を使用bind
方法を見てみましょう。
> run (bind digit (fun v -> digit)) "123";;
val it : char option * int = (Some '2', 2)
> run (bind digit (fun v -> bind digit (fun u -> return_ (v,u)))) "123";;
val it : (char * char) option * int = (Some ('1', '2'), 2)
> run (bind digit (fun v -> bind digit (fun u -> return_ (v,u)))) "1";;
val it : (char * char) option * int = (null, 1)
これが私たちに示していることは、 bind
が2つのパーサーをより複雑なパーサーにbind
できることです。 bind
の結果はパーサーであり、これは次に再び組み合わせることができます。
> run (bind digit (fun v -> bind digit (fun w -> bind digit (fun u -> return_ (v,w,u))))) "123";;
val it : (char * char * char) option * int = (Some ('1', '2', '3'), 3)
bind
はパーサーを結合する基本的な方法ですが、構文を単純化するヘルパー関数を定義します。
構文を単純化できるものの1つに、 計算式があります。彼らは簡単に定義することができます:
type ParserBuilder() =
member x.Bind (t, uf) = bind t uf
member x.Return v = return_ v
member x.ReturnFrom t = t
// 'parser' enables us to combine parsers using 'parser { ... }' syntax
let parser = ParserBuilder()
FSI
let p = parser {
let! v = digit
let! u = digit
return v,u
}
run p "123"
val p : Parser<char * char> = Parser <fun:bind@49-1>
val it : (char * char) option * int = (Some ('1', '2'), 2)
これは次のようになります。
> let p = bind digit (fun v -> bind digit (fun u -> return_ (v,u)))
run p "123";;
val p : Parser<char * char> = Parser <fun:bind@49-1>
val it : (char * char) option * int = (Some ('1', '2'), 2)
orElse
を使う別の基本的なパーサーコンビネータは、 orElse
です。
// 'orElse' creates a parser that runs parser 't' first, if that is successful
// the result is returned otherwise the result of parser 'u' is returned
let orElse t u = Parser <| fun (s, p) ->
let (Parser tps) = t
let tov, tp = tps (s, p)
match tov with
| None ->
let (Parser ups) = u
ups (s, p)
| Some tv -> succeedWith tv tp
これにより、 letterOrDigit
ように定義することができます。
> let letterOrDigit = orElse letter digit;;
val letterOrDigit : Parser<char> = Parser <fun:orElse@70-1>
> run letterOrDigit "123";;
val it : char option * int = (Some '1', 1)
> run letterOrDigit "hello";;
val it : char option * int = (Some 'h', 1)
> run letterOrDigit "!!!";;
val it : char option * int = (null, 0)
インフィックス演算子に関する注意
FPに対する一般的な懸念は、 >>=
、 >=>
、 <-
などのような特殊な挿入演算子の使用です。しかし、ほとんどの場合、 +
、 -
、 *
、 /
および%
の使用には関心がありませんが、これは値を構成するのに使用されるよく知られた演算子です。しかし、FPの大きな部分は値だけでなく機能も構成することです。中間のFP開発者には、中置演算子>>=
、 >=>
、 <-
がよく知られており、特定のシグネチャとセマンティクスを持つ必要があります。
ここまで定義した関数に対して、パーサーを結合するために使用される次の中置演算子を定義します。
let (>>=) t uf = bind t uf
let (<|>) t u = orElse t u
したがって、 >>=
はbind
を意味bind
、 <|>
はorElse
意味しorElse
。
これにより、パーザをより簡潔に組み合わせることができます。
let letterOrDigit = letter <|> digit
let p = digit >>= fun v -> digit >>= fun u -> return_ (v,u)
もっと複雑な式を解析できる高度なパーサーコンビネータを定義するために、より単純なパーサコンビネータをいくつか定義します。
// 'map' runs parser 't' and maps the result using 'm'
let map m t = t >>= (m >> return_)
let (>>!) t m = map m t
let (>>%) t v = t >>! (fun _ -> v)
// 'opt' takes a parser 't' and creates a parser that always succeed but
// if parser 't' fails the new parser will produce the value 'None'
let opt t = (t >>! Some) <|> (return_ None)
// 'pair' runs parser 't' and 'u' and returns a pair of 't' and 'u' results
let pair t u =
parser {
let! tv = t
let! tu = u
return tv, tu
}
私たちはmany
sepBy
を定義する準備ができていますsepBy
は、入力パーザが失敗するまで適用されるので、より高度です。次に、 many
とsepBy
は集計結果を返します。
// 'many' applies parser 't' until it fails and returns all successful
// parser results as a list
let many t =
let ot = opt t
let rec loop vs = ot >>= function Some v -> loop (v::vs) | None -> return_ (List.rev vs)
loop []
// 'sepBy' applies parser 't' separated by 'sep'.
// The values are reduced with the function 'sep' returns
let sepBy t sep =
let ots = opt (pair sep t)
let rec loop v = ots >>= function Some (s, n) -> loop (s v n) | None -> return_ v
t >>= loop
単純な式パーサーの作成
作成したツールを使って、 1+2*3
ような単純な式のパーサを定義できるようになりました
一番下から始めて、整数pint
パーサーを定義します
// 'pint' parses an integer
let pint =
let f s v = 10*s + int v - int '0'
parser {
let! digits = many digit
return!
match digits with
| [] -> fail
| vs -> return_ (List.fold f 0 vs)
}
可能な限り多くの数字を解析しようとしchar list
。結果はchar list
です。リストが空の場合はfail
、それ以外の場合は文字を整数に変換します。
FSIでのpint
テスト:
> run pint "123";;
val it : int option * int = (Some 123, 3)
さらに、整数値を結合するために使用される異なる種類の演算子を解析する必要があります。
// operator parsers, note that the parser result is the operator function
let padd = char '+' >>% (+)
let psubtract = char '-' >>% (-)
let pmultiply = char '*' >>% (*)
let pdivide = char '/' >>% (/)
let pmodulus = char '%' >>% (%)
FSI:
> run padd "+";;
val it : (int -> int -> int) option * int = (Some <fun:padd@121-1>, 1)
すべて一緒に結ぶ:
// 'pmullike' parsers integers separated by operators with same precedence as multiply
let pmullike = sepBy pint (pmultiply <|> pdivide <|> pmodulus)
// 'paddlike' parsers sub expressions separated by operators with same precedence as add
let paddlike = sepBy pmullike (padd <|> psubtract)
// 'pexpr' is the full expression
let pexpr =
parser {
let! v = paddlike
let! _ = eos // To make sure the full string is consumed
return v
}
それをFSIですべて実行する:
> run pexpr "2+123*2-3";;
val it : int option * int = (Some 245, 9)
結論
Parser<'T>
、 return_
、 bind
をreturn_
、それらがモナドの法則に従っていることを確認することによって、単純で強力なMonadic Parser Combinatorフレームワークを構築しました。
パーサーはパーサー状態で実行されるため、MonadsとParserは一緒になります。 Monadsを使うと、パーサの状態を隠しながらパーザを組み合わせることができ、混乱を減らし、合成性を向上させることができます。
私たちが作成したフレームワークは遅く、エラーメッセージも生成されません。これは、コードを簡潔にするためです。 FParsecは、許容できるパフォーマンスと優れたエラーメッセージの両方を提供します。
しかし、一例だけでは、モナドの理解を得ることはできません。モナドを練習しなければならない。
あなたの勝利の理解に達するために実装しようとすることができるMonadsに関するいくつかの例があります:
- 状態モナド - 隠された環境状態を暗黙的に運ぶことができる
- Tracer Monad - トレース状態を暗黙的に伝えることができます。国家モナドの変種
- Turtle Monad - Turtle(Logos)プログラムを作成するためのモナド。国家モナドの変種
- 継続モナド - コルーチンモナド。これの例は、F#で
async
です。
あなたが快適であるドメイン内のMonadsのためのアプリケーションを考え出すことが、最高のことです。それはパーサーでした。
完全なソースコード:
// A Parser<'T> is a function that takes a string and position
// and returns an optionally parsed value and a position
// A parsed value means the position points to the character following the parsed value
// No parsed value indicates a parse failure at the position
type Parser<'T> = Parser of (string*int -> 'T option*int)
// Runs a parser 't' on the input string 's'
let run t s =
let (Parser tps) = t
tps (s, 0)
// Different ways to create parser result
let succeedWith v p = Some v, p
let failAt p = None , p
// The 'satisfy' parser succeeds if the character at the current position
// passes the 'sat' function
let satisfy sat : Parser<char> = Parser <| fun (s, p) ->
if p < s.Length && sat s.[p] then succeedWith s.[p] (p + 1)
else failAt p
// 'eos' succeeds if the position is beyond the last character.
// Useful when testing if the parser have consumed the full string
let eos : Parser<unit> = Parser <| fun (s, p) ->
if p < s.Length then failAt p
else succeedWith () p
let anyChar = satisfy (fun _ -> true)
let char ch = satisfy ((=) ch)
let digit = satisfy System.Char.IsDigit
let letter = satisfy System.Char.IsLetter
// 'fail' is a parser that always fails
let fail<'T> = Parser <| fun (s, p) -> failAt p
// 'return_' is a parser that always succeed with value 'v'
let return_ v = Parser <| fun (s, p) -> succeedWith v p
// 'bind' let us combine two parser into a more complex parser
let bind t uf = Parser <| fun (s, p) ->
let (Parser tps) = t
let tov, tp = tps (s, p)
match tov with
| None -> None, tp
| Some tv ->
let u = uf tv
let (Parser ups) = u
ups (s, tp)
type ParserBuilder() =
member x.Bind (t, uf) = bind t uf
member x.Return v = return_ v
member x.ReturnFrom t = t
// 'parser' enables us to combine parsers using 'parser { ... }' syntax
let parser = ParserBuilder()
// 'orElse' creates a parser that runs parser 't' first, if that is successful
// the result is returned otherwise the result of parser 'u' is returned
let orElse t u = Parser <| fun (s, p) ->
let (Parser tps) = t
let tov, tp = tps (s, p)
match tov with
| None ->
let (Parser ups) = u
ups (s, p)
| Some tv -> succeedWith tv tp
let (>>=) t uf = bind t uf
let (<|>) t u = orElse t u
// 'map' runs parser 't' and maps the result using 'm'
let map m t = t >>= (m >> return_)
let (>>!) t m = map m t
let (>>%) t v = t >>! (fun _ -> v)
// 'opt' takes a parser 't' and creates a parser that always succeed but
// if parser 't' fails the new parser will produce the value 'None'
let opt t = (t >>! Some) <|> (return_ None)
// 'pair' runs parser 't' and 'u' and returns a pair of 't' and 'u' results
let pair t u =
parser {
let! tv = t
let! tu = u
return tv, tu
}
// 'many' applies parser 't' until it fails and returns all successful
// parser results as a list
let many t =
let ot = opt t
let rec loop vs = ot >>= function Some v -> loop (v::vs) | None -> return_ (List.rev vs)
loop []
// 'sepBy' applies parser 't' separated by 'sep'.
// The values are reduced with the function 'sep' returns
let sepBy t sep =
let ots = opt (pair sep t)
let rec loop v = ots >>= function Some (s, n) -> loop (s v n) | None -> return_ v
t >>= loop
// A simplistic integer expression parser
// 'pint' parses an integer
let pint =
let f s v = 10*s + int v - int '0'
parser {
let! digits = many digit
return!
match digits with
| [] -> fail
| vs -> return_ (List.fold f 0 vs)
}
// operator parsers, note that the parser result is the operator function
let padd = char '+' >>% (+)
let psubtract = char '-' >>% (-)
let pmultiply = char '*' >>% (*)
let pdivide = char '/' >>% (/)
let pmodulus = char '%' >>% (%)
// 'pmullike' parsers integers separated by operators with same precedence as multiply
let pmullike = sepBy pint (pmultiply <|> pdivide <|> pmodulus)
// 'paddlike' parsers sub expressions separated by operators with same precedence as add
let paddlike = sepBy pmullike (padd <|> psubtract)
// 'pexpr' is the full expression
let pexpr =
parser {
let! v = paddlike
let! _ = eos // To make sure the full string is consumed
return v
}
計算式はChain Monadの代替構文を提供します
Monadsに関連するのはF#
計算式 ( CE
)です。プログラマは、典型的には、Monadsを連鎖する別のアプローチを提供するためにCE
を実装しCE
。
let v = m >>= fun x -> n >>= fun y -> return_ (x, y)
あなたはこれを書くことができます:
let v = ce {
let! x = m
let! y = n
return x, y
}
どちらのスタイルも同等で、どちらを選ぶかは開発者の好みに依存します。
CE
を実装する方法を示すために、すべてのトレースに相関IDを含めることが好きです。この相関IDは、同じ呼び出しに属するトレースを相関させるのに役立ちます。これは、同時呼び出しのトレースを含むログファイルを持つ場合に非常に便利です。
問題は、相関IDをすべての関数の引数として含めるのは面倒です。 Monads が暗黙の状態を運ぶことができるように 、Log Monadを定義してログコンテキスト(すなわち相関ID)を非表示にします。
まず、ログコンテキストと、ログコンテキストをトレースする関数のタイプを定義します。
type Context =
{
CorrelationId : Guid
}
static member New () : Context = { CorrelationId = Guid.NewGuid () }
type Function<'T> = Context -> 'T
// Runs a Function<'T> with a new log context
let run t = t (Context.New ())
また、ログコンテキストから相関IDを使用してログする2つのトレース関数を定義します。
let trace v : Function<_> = fun ctx -> printfn "CorrelationId: %A - %A" ctx.CorrelationId v
let tracef fmt = kprintf trace fmt
trace
は、呼び出されたときにログコンテキストが渡されることを意味するFunction<unit>
です。ログコンテキストから相関IDを取得し、それをv
と一緒にトレースします
さらに、 bind
とreturn_
を定義し、 Monadの法則に従い、これがLog Monadを形成します。
let bind t uf : Function<_> = fun ctx ->
let tv = t ctx // Invoke t with the log context
let u = uf tv // Create u function using result of t
u ctx // Invoke u with the log context
// >>= is the common infix operator for bind
let inline (>>=) (t, uf) = bind t uf
let return_ v : Function<_> = fun ctx -> v
最後に、 LogBuilder
を定義して、 Log
Monadsを連鎖させるためにCE
構文を使用できるようにします。
type LogBuilder() =
member x.Bind (t, uf) = bind t uf
member x.Return v = return_ v
// This enables us to write function like: let f = log { ... }
let log = Log.LogBuilder ()
暗黙のログコンテキストを持つ関数を定義できるようになりました。
let f x y =
log {
do! Log.tracef "f: called with: x = %d, y = %d" x y
return x + y
}
let g =
log {
do! Log.trace "g: starting..."
let! v = f 1 2
do! Log.tracef "g: f produced %d" v
return v
}
gをgと実行します。
printfn "g produced %A" (Log.run g)
どちらが印刷されますか:
CorrelationId: 33342765-2f96-42da-8b57-6fa9cdaf060f - "g: starting..."
CorrelationId: 33342765-2f96-42da-8b57-6fa9cdaf060f - "f: called with: x = 1, y = 2"
CorrelationId: 33342765-2f96-42da-8b57-6fa9cdaf060f - "g: f produced 3"
g produced 3
CorrelationIdがrun
からg
にf
暗黙的に持ち込まれることに注意してください。これにより、トラブルシューティング時にログエントリを関連付けることができます。
CE
はさらに多くの機能がありますが、これはあなた自身のCE
定義するのに役立ちます。
フルコード:
module Log =
open System
open FSharp.Core.Printf
type Context =
{
CorrelationId : Guid
}
static member New () : Context = { CorrelationId = Guid.NewGuid () }
type Function<'T> = Context -> 'T
// Runs a Function<'T> with a new log context
let run t = t (Context.New ())
let trace v : Function<_> = fun ctx -> printfn "CorrelationId: %A - %A" ctx.CorrelationId v
let tracef fmt = kprintf trace fmt
let bind t uf : Function<_> = fun ctx ->
let tv = t ctx // Invoke t with the log context
let u = uf tv // Create u function using result of t
u ctx // Invoke u with the log context
// >>= is the common infix operator for bind
let inline (>>=) (t, uf) = bind t uf
let return_ v : Function<_> = fun ctx -> v
type LogBuilder() =
member x.Bind (t, uf) = bind t uf
member x.Return v = return_ v
// This enables us to write function like: let f = log { ... }
let log = Log.LogBuilder ()
let f x y =
log {
do! Log.tracef "f: called with: x = %d, y = %d" x y
return x + y
}
let g =
log {
do! Log.trace "g: starting..."
let! v = f 1 2
do! Log.tracef "g: f produced %d" v
return v
}
[<EntryPoint>]
let main argv =
printfn "g produced %A" (Log.run g)
0