F# でモナドを扱う場合、「コンピュテーション式 (Computation Expressions, 計算式)」を使うと便利です。コンピュータ (F#) ではいろいろな計算が行われます。計算方法は逐次処理だけではなく並行 (並列) 処理がありますし、正格評価や遅延評価、リストモナドのような非決定性計算もあります。このような計算方法を抽象化するための機能がコンピュテーション式です。
F# には以下に示す組み込みのコンピュテーション式が用意されています。
「シーケンス」と「遅延評価 (lazy 式)」は拙作のページで取り上げました。このほかにも、F# では自分でコンピュテーション式を定義することができます。今回はモナドを例題にコンピュテーション式の基本的な使い方について簡単に説明します。
コンピュテーション式はクラスから生成します。クラスを定義するとき、その中で特殊なメソッドを定義すると、そのクラスはコンピュテーション式を生成するクラスとして扱われます。これを「ビルダークラス」といいます。
モナドの場合、基本的な動作は以下に示す 2 つのメソッドを定義するだけです。
型はジェネリックで表記されているので難しくみえますが、Bind の型はモナドそのものです。最初の引数 M<'T> はデータを格納する型 (モナド) をジェネリックで表しています。もう一つの引数の型は 'T -> M<'U> なので、値を同じモナドに格納して返すことが分かります。Return は値をモナドに包んで返します。値を x とすると、option であれば Some x に、Result であれば Ok x に、List であれば [x] となります。
このほかに、メソッド Zero と ReturnFrom を定義しておくと便利です。
ReturnFrom は引数 (モナド) をそのまま返します。Zero は if 式で else が省略されたときに呼び出されます。option, Reuslt, List でいえば失敗を表すデータ None, Error, [] を返します。
コンピュテーション式 cexpr の中では、以下に示す特別な構文を使用することができます。
cexpr { let! ... }
cexpr { do! ... }
cexpr { yield ... }
cexpr { yield! ... }
cexpr { return ... }
cexpr { return! ... }
cexpr { match! ... }
モナドで使用するのは let!, do!, return, return! です。let! と do! は Bind で指定した関数を呼び出します。関数を bind func expr とすると、let! と do! はおおむね次のように動作します。
1. cexpr {
let! x = expr
...
}
=> expr |> bind (fun x -> ...)
2. cexpr {
do! expr
...
}
=> expr |> bind (fun _ -> ...)
let! と do! は式 expr を評価します。その結果はモナドになります。let! はそこから値を取り出して変数にセットします。do! は取り出した値を捨て去ります。そして、それ以降に記述されている式 ... を順番に実行します。つまり、それ以降で let! や do! を記述すれば、各処理を連結していくことができるわけです。
値を返すときは return または return! を使います。return はメソッド Return で指定した処理を、return! はメソッド ReturnFrom で指定した処理を行います。つまり、return は引数をモナドに包んで返し、return! は引数をそのまま返します。
それでは実際に、コンピュテーション式を使って option, Result, List のモナドを作ってみましょう。
リスト : コンピュテーション式によるモナドの実装 (compexpr.fsx) // option type OptionMonadBuilder() = member this.Bind(m, f) = Option.bind f m member this.Return(x) = Some x let optionMonad = OptionMonadBuilder() // Result type ResultMonadBuilder() = member this.Bind(m, f) = Result.bind f m member this.Return(x) = Ok x let resultMonad = ResultMonadBuilder() // List type ListMonadBuilder() = member this.Bind(m, f) = List.collect f m member this.Return(x) = [x] member this.ReturnFrom(x) = x member this.Zero() = [] let listMonad = ListMonadBuilder() let guard = function true -> [()] | false -> []
> #load "compexpr.fsx";;
[読み込み中 /home/mhiroi/fsharp/compexpr.fsx]
namespace FSI_0002.Compexpr
type OptionMonadBuilder =
new: unit -> OptionMonadBuilder
member Bind: m: 'b option * f: ('b -> 'c option) -> 'c option
member Return: x: 'a -> 'a option
val optionMonad: OptionMonadBuilder
type ResultMonadBuilder =
new: unit -> ResultMonadBuilder
member Bind: m: Result<'c,'d> * f: ('c -> Result<'e,'d>) -> Result<'e,'d>
member Return: x: 'a -> Result<'a,'b>
val resultMonad: ResultMonadBuilder
type ListMonadBuilder =
new: unit -> ListMonadBuilder
member Bind: m: 'd list * f: ('d -> 'e list) -> 'e list
member Return: x: 'c -> 'c list
member ReturnFrom: x: 'b -> 'b
member Zero: unit -> 'a list
val listMonad: ListMonadBuilder
val guard: _arg1: bool -> unit list
Bind はモジュールに用意されている関数 Option.bind, Result.bind, List.collect を使えば簡単です。あとは、ビルダークラスを評価して、その結果を変数にセットします。変数名がコンピュテーション式の名前になります。あとは 名前 { ... } でコンピュテーション式を使用することができます。
それでは実行してみましょう。
> open Compexpr;;
> let add x y =
- optionMonad {
- let! a = x
- let! b = y
- return (a + b)
- };;
val add: x: int option -> y: int option -> int option
> add (Some 10) (Some 20);;
val it: int option = Some 30
> add (Some 10) None;;
val it: int option = None
> add None (Some 20);;
val it: int option = None
> let resultAdd x y =
- resultMonad {
- let! a = x
- let! b = y
- return (a + b)
- };;
val resultAdd: x: Result<int,'a> -> y: Result<int,'a> -> Result<int,'a>
> resultAdd ((Ok 10):Result<int,string>) ((Ok 20):Result<int,string>);;
val it: Result<int,string> = Ok 30
> resultAdd ((Ok 10):Result<int,string>) ((Error "oops"):Result<int,string>);;
val it: Result<int,string> = Error "oops"
> resultAdd ((Error "oops"):Result<int,string>) ((Ok 20):Result<int,string>);;
val it: Result<int,string> = Error "oops"
関数 add と resultAdd は、引数に option, Result を受け取り、その値を足し算します。どちらも let! で引数から値を取り出し、加算した値を return で返します。引数が None や Error であれば、None や Error を返します。モナドとして正常に動作していますね。
> listMonad {
- let! x = [1..4]
- let! y = [11..14]
- return (x, y)
- };;
val it: (int * int) list =
[(1, 11); (1, 12); (1, 13); (1, 14); (2, 11); (2, 12); (2, 13); (2, 14);
(3, 11); (3, 12); (3, 13); (3, 14); (4, 11); (4, 12); (4, 13); (4, 14)]
> listMonad {
- let! x = [1..4]
- let! y = [11..14]
- do! guard(x + y > 15)
- return (x, y)
- };;
val it: (int * int) list =
[(2, 14); (3, 13); (3, 14); (4, 12); (4, 13); (4, 14)]
> listMonad {
- let! x = [1..4]
- let! y = [11..14]
- if x + y > 15 then return (x, y)
- };;
val it: (int * int) list =
[(2, 14); (3, 13); (3, 14); (4, 12); (4, 13); (4, 14)]
リストモナドの場合、コンピュテーション式でも内包表記のように使うことができます。do! を使ってガード (関数 guard) を呼び出すこともできます。今回は Zero() を定義してあるので、同様なことを if で行うこともできます。
もちろん、リストモナドで順列を生成することも簡単です。
リスト : 順列の生成
let permMonad n (xs: 'a list) =
let rec iter m ys =
listMonad {
if m = n then return (List.rev ys)
else
let! x = xs
do! guard(not(List.contains x ys))
return! (iter (m + 1) (x::ys))
}
iter 0 []
> permMonad 3 [1..3];; val it: int list list = [[1; 2; 3]; [1; 3; 2]; [2; 1; 3]; [2; 3; 1]; [3; 1; 2]; [3; 2; 1]] > permMonad 3 [1..4];; val it: int list list = [[1; 2; 3]; [1; 2; 4]; [1; 3; 2]; [1; 3; 4]; [1; 4; 2]; [1; 4; 3]; [2; 1; 3]; [2; 1; 4]; [2; 3; 1]; [2; 3; 4]; [2; 4; 1]; [2; 4; 3]; [3; 1; 2]; [3; 1; 4]; [3; 2; 1]; [3; 2; 4]; [3; 4; 1]; [3; 4; 2]; [4; 1; 2]; [4; 1; 3]; [4; 2; 1]; [4; 2; 3]; [4; 3; 1]; [4; 3; 2]]
局所関数 iter の返り値はモナドなので、return! で値を返すことに注意してください。
最後に簡単な論理パズルを解いてみましょう。
3人の友達が、あるプログラミング競技会で1位、2位、3位になった。この3人は、名前も、好きなスポーツも、国籍も異なる。Michael はバスケットが好きで、アメリカ人よりも上位であった。イスラエル人の Simon はテニスをする者よりも上位であった。クリケットをするものが1位であった。誰がオーストラリア人か? Richard はどのようなスポーツをするか?
簡単な論理パズルなので、プログラムを作る前に考えてみてください。
最初に必要となる型を定義しましょう。
リスト : 型の定義
type Nation = US | IL | AU
type Sports = Basket | Cricket | Tennis
type Name = Michael | Simon | Richard
type Person = {name: Name; rank: int; nation: Nation; sports: Sports}
このデータをリストモナドで作成します。次のリストを見てください。
リスト : データの生成
let makePerson name =
listMonad {
let! r = [1; 2; 3]
let! n = [US; IL; AU]
let! s = [Basket; Cricket; Tennis]
return {name = name; rank = r; nation = n; sports = s}
}
let! で順位 (1, 2, 3)、国籍 (US, IL, AU)、スポーツ (Basket, Cricket, Tennis) の中から要素を一つ選びます。あとは、レコード Person を生成して return で返すだけです。
次は問題を解くための補助関数を作ります。
リスト : 補助関数の定義 // 国籍を探す let findNation n xs = List.find (fun p -> p.nation = n) xs // スポーツを探す let findSports s xs = List.find (fun p -> p.sports = s) xs // 名前以外で重複した要素があるか let isDuplicate x y = x.rank = y.rank || x.nation = y.nation || x.sports = y.sports // 同じ要素があるか let check a b c = isDuplicate a b || isDuplicate a c || isDuplicate b c
findNation は引数のリスト中から国籍が引数 n と等しい要素を返します。findSports は好きなスポーツが引数 s と等しい要素を返します。isDuplicate は引数 a と b に重複した要素があれば true を返します。要素が全て異なる場合は false を返します。関数 check は isDuplicate を呼び出して、引数 a, b, c に重複した要素があれば true を返します。
論理パズルの解法プログラムは次のようになります。
リスト : 論理パズルの解法
let solver () =
listMonad {
let! m = makePerson(Michael)
let! s = makePerson(Simon)
let! r = makePerson(Richard)
do! guard(not(check m s r))
do! guard(m.sports = Basket)
do! guard(m.nation <> US)
do! guard(s.nation = IL)
do! guard(m.rank < (findNation US [m; s; r]).rank)
do! guard(s.rank < (findSports Tennis [m; s; r]).rank)
do! guard((findSports Cricket [m; s; r]).rank = 1)
return (m, s, r)
}
最初に makePerson でデータを作成し、let! で要素を一つ選んで局所変数 m, s, r にセットします。そして、check で順位、国籍、スポーツで要素が重複していないかチェックします。あとは問題の条件を guard でチェックしていくだけです。
最後に、return で見つけた解をタプルに格納して返します。とても簡単ですね。実行結果は次のようになります。
> solver();;
val it: (Person * Person * Person) list =
[({ name = Michael
rank = 2
nation = AU
sports = Basket }, { name = Simon
rank = 1
nation = IL
sports = Cricket }, { name = Richard
rank = 3
nation = US
sports = Tennis })]
解は 1 通りで、1位が Simon, 2位が Michael, 3位が Richard になります。ちなみに、最後の条件がない場合は 2 通りの解が出力されます。興味のある方は試してみてください。
//
// compexpr.fsx : コンピュテーション式によるモナドの実装
//
// Copyright (C) 2022 Makoto Hiroi
//
// option
type OptionMonadBuilder() =
member this.Bind(m, f) = Option.bind f m
member this.Return(x) = Some x
let optionMonad = OptionMonadBuilder()
// Result
type ResultMonadBuilder() =
member this.Bind(m, f) = Result.bind f m
member this.Return(x) = Ok x
let resultMonad = ResultMonadBuilder()
// List
type ListMonadBuilder() =
member this.Bind(m, f) = List.collect f m
member this.Return(x) = [x]
member this.ReturnFrom(x) = x
member this.Zero() = []
let listMonad = ListMonadBuilder()
// ガード
let guard = function
true -> [()]
| false -> []
// 順列の生成
let permMonad n (xs: 'a list) =
let rec iter m ys =
listMonad {
if m = n then return (List.rev ys)
else
let! x = xs
do! guard(not(List.contains x ys))
return! (iter (m + 1) (x::ys))
}
iter 0 []
// 論理パズル
type Nation = US | IL | AU
type Sports = Basket | Cricket | Tennis
type Name = Michael | Simon | Richard
type Person = {name: Name; rank: int; nation: Nation; sports: Sports}
let makePerson name =
listMonad {
let! r = [1; 2; 3]
let! n = [US; IL; AU]
let! s = [Basket; Cricket; Tennis]
return {name = name; rank = r; nation = n; sports = s}
}
// 国籍を探す
let findNation n xs =
List.find (fun p -> p.nation = n) xs
// スポーツを探す
let findSports s xs =
List.find (fun p -> p.sports = s) xs
// 名前以外で重複した要素があるか
let isDuplicate x y =
x.rank = y.rank || x.nation = y.nation || x.sports = y.sports
// 同じ要素があるか
let check a b c =
isDuplicate a b || isDuplicate a c || isDuplicate b c
let solver () =
listMonad {
let! m = makePerson(Michael)
let! s = makePerson(Simon)
let! r = makePerson(Richard)
do! guard(not(check m s r))
do! guard(m.sports = Basket)
do! guard(m.nation <> US)
do! guard(s.nation = IL)
do! guard(m.rank < (findNation US [m; s; r]).rank)
do! guard(s.rank < (findSports Tennis [m; s; r]).rank)
do! guard((findSports Cricket [m;s;r]).rank = 1)
return (m, s, r)
}