StateモナドとMaybeモナドを合成すれば、状態を変化させながら失敗したときに打ち切ることが可能です。モナド変換子まで実装するのは大変なので、モナドに拘らずに目的に特化したコンピュテーション式を作ってみようと考えました。
この記事は次の続編です。
- 不揃いなデータをコンピュテーション式で処理 2016.06.30
上に掲載したコンピュテーション式を、どうやって作ったかという過程を説明します。あまり定義には深入りせずに、少しずつ変形して得ました。
また、修正して頂いたコードについても考察します。関数モナドに近いのではないかと思いました。
発端
Stateモナドでは状態を取得するのにget
を使用します。動作決め打ちのコンピュテーション式を作れば、get
のようなアクションが不要になるのではないかと考えました。
type GetBuilder() =
member this.Bind(_, f) = fun s -> (f s) s
member this.Zero() = fun _ -> ()
let get = GetBuilder()
let test = get {
let! a = () in printfn "%d" a
let! a = () in printfn "%d" a }
test 1
test 2
1
1
2
2
改造
値を取得する度に+ 1
することを試してみました。
type IncBuilder() =
member this.Bind(_, f) = fun s -> (f s) (s + 1)
member this.Zero() = fun _ -> ()
let inc = IncBuilder()
let test = inc {
let! a = () in printfn "%d" a
let! a = () in printfn "%d" a }
test 1
test 2
1
2
2
3
Bind
の実装で、s
はf
への引数、(s + 1)
は次のブロックに渡す状態です。
※ 状態系モナドでは次のブロックに状態を渡すという解釈が、細かいトレースに煩わされずに形式的に扱うためのコツのような気がしています。
リスト
リストの先頭から1つずつ取り出すように修正します。
type ListReaderBuilder() =
member this.Bind(_, f) = fun (x::xs) -> (f x) xs
member this.Zero() = fun _ -> ()
let listReader = ListReaderBuilder()
let test = listReader {
let! a = () in printfn "%d" a
let! a = () in printfn "%d" a }
test [1;2;3]
printfn "---"
test [4;5;6]
1
2
---
4
5
※ パターンマッチの不備で警告されます。
脱出
途中でリストが尽きたら脱出するように修正します。(Maybe風味)
type ListReaderBuilder() =
member this.Bind(_, f) = function x::xs -> f x xs | _ -> ()
member this.Zero() = fun _ -> ()
let listReader = ListReaderBuilder()
let test = listReader {
let! a = () in printfn "%d" a
let! a = () in printfn "%d" a }
test [1;2;3]
printfn "---"
test [4]
1
2
---
4
修正
@pocketberserkerさんによる修正です。
Bind
の第1引数を捨てずに評価すれば、do!
で簡単に書けるというご指摘です。
type ListReaderBuilder() =
member this.Bind(g, f) = function (x::xs) -> (f (g x)) xs | _ -> ()
member this.Return(_) = fun _ -> ()
let listReader = ListReaderBuilder()
let test = listReader {
do! printfn "%d"
do! printfn "%d"
}
test [1;2;3]
printfn "---"
test [4]
1
2
---
4
値が取れなくなるのではないかと懸念しましたが、id
で普通に取れました。
関数モナド
修正していただいたコンピュテーション式の意味を考えましたが、関数モナドが出発点なのではないかと気付きました。確認するために関数モナドを作ってみた副産物が次の記事です。
- doブロックとコンピュテーション式 2016.07.01
実際、関数モナドを使えば、値こそ変化しないものの似たことができます。
type FuncBuilder() =
member __.Bind(m, f) = fun x -> f (m x) x
member __.Return(x) = fun _ -> x
let func = FuncBuilder()
let test = func {
do! printfn "%A"
do! printfn "%A"
}
test [1;2;3]
printfn "---"
test [4]
[1; 2; 3]
[1; 2; 3]
---
[4]
[4]
これをリストが変化するように改造すれば、修正して頂いたコードが得られます。
まとめ
アクションを消そうという発想で出発しましたが、関数モナドに揺り戻されました。何だかんだ言っても、やはり定石は手堅いです。