F#プログラマのためのMaybeモナド入門


http://twitpic.com/3w34bo

はじめに

モナドといえばHaskellHaskellといえばモナドが有名ですが、モナドは特定の言語とは無関係の仕組みですので、F#でも使えます。ただ単に使えるだけでなくコンピュテーション式というモナドをより便利に使うための文法まで用意されています。
この記事では、option型を便利に扱うためのMaybeモナドを例にモナドのすごさを紹介していきます。

Maybeモナドのすごいとこ

option型っていいですよね。ぬるぽので落ちる心配もないですし。Maybeモナドはそんなoption型を便利に扱うためのモナドです。

option型を扱うのにMaybeモナドと呼ばれてるのは、Haskell由来だからで特に重要な理由はありません。

DBからの値取得とかHttpリクエストからのパラメータ取得などの外部とやりとりする関数は必ず成功するとか限らないのでoption型を返す関数として実装するのが安全です。でもoption型を返す関数を繋げていくとネストが深く、読みづらくなってしまいます。

例えば、 dbというMapに格納されている"x"と"y"を加算する関数は次のようになります。

(* 値が格納されたMapを作る *)
let db = Map.ofList [("x", 1); ("y", 2); ("z", 3)]

(* dbに格納されている"x"と"y"を加算する関数 *)
let x_and_y =
    match Map.tryFind "x" db with
    | Some x ->
        match Map.tryFind "y" db with
        | Some y ->
            (* xとyが取得できたので加算して返す *)
            Some (x + y)
        | None ->
            (* yの取得に失敗したのでNoneを返す *)
            None
    | None ->
        (* xの取得に失敗したのでNoneを返す *)
        None

(*
  実行結果
  val it : int option = Some 3
 *)

ネストが深いくて読みづらいですね。この例は2つだからまだいいですが、3つや4つの値を扱うことを考えるとげんなりしてきます。

これをMaybeモナドを使って書き直すと次のようになります。

let x_and_y' =    maybe {
        let! x = Map.tryFind "x" db (* 失敗したら全体がNoneになる *)
        let! y = Map.tryFind "y" db (* 失敗したら全体がNoneになる *)
        return x + y (* xとyを加算して返す *)
    }

(*
  実行結果
  val it : int option = Some 3
 *)

ネストが浅くなってるしコードの量も減って読みやすくなっています。 しかもMap.tryFindのどちらかがNoneを返したらx_and_y'もNoneになるので、安全性も失われていません。

さあ、みんなもMaybeモナドを使いましょう!

Maybeモナドの裏側


http://www.flickr.com/photos/audra_b/3831200/

さて、なぜこんなことができるのかを見ていきましょう。

コンピュテーション式による変換

maybe { ... }はコンピュテーション式と呼ばれる構文で、コンパイル時に次のような変換が行なわれます。(一部抜粋)

{¦ let! pattern = expr in cexpr ¦} builder.Bind(expr, (fun pattern -> {¦ cexpr ¦}))
{¦ return expr ¦} builder.Return(expr)

おおまかに言うとlet!がbuilder.Bindに、returnがbuilder.Returnに変換されます。

この変換規則に従って、先程の式は次のように変換されます。

(*
先程の式:
 let x_and_y' =     maybe {
         let! x = Map.tryFind "x" db
         let! y = Map.tryFind "y" db
         return x + y
     }
*)

(* コンピュテーション式を展開した式 *)
let x_and_y' =
    (* let! x = Map.tryFind "x" db *)
    maybe.Bind(Map.tryFind "x" db, fun x ->
      (* let! y = Map.tryFind "y" db *)
      maybe.Bind(Map.tryFind "y" db, fun y ->
         (* return x + y *)
         maybe.Return(x+y)))
Maybeモナドの定義

ここでのmaybeはBindとReturnを実装したMaybeBuilderクラスのインスタンスです。MaybeBuilderクラスは次のように定義されています。

type MaybeBuilder() =
    member this.Bind(x, f) =
        match x with
        | Some y -> f y
        | None -> None

    member this.Return(x) =
        Some x

let maybe = new MaybeBuilder()

コンピュテーション式を展開した式のBindとReturnも展開すると、次のようになります。

(* コンピュテーション式を展開した式
 let x_and_y' =
     maybe.Bind(Map.tryFind "x" db, fun x ->
       maybe.Bind(Map.tryFind "y" db, fun y ->
          maybe.Return(x+y)))
 *)
let x_and_y' =
    (* maybe.Bind(Map.tryFind "x" db, fun x -> ...) *)
    match Map.tryFind "x" db with
    | Some x ->
        (* maybe.Bind(Map.tryFind "y" db, fun y -> ...) *)
        match Map.tryFind "y" db with
        | Some y ->
            (* maybe.Return(x+y) *)
            Some (x + y)
        | None ->
            None
    | None ->
        None

結局、モナドを使わない場合と同じ式になりました。

まとめ:なにがすごいか

Maybeモナドを使うことで、optionを扱うコードを短く書くことができました。

今回はoptionを扱うコードを短く書くMaybeモナドを紹介しただけですが、他にも状態付きの計算を行なうためのStateモナドや非決定計算を行なうためのListモナドなどがあります。詳しくはAll About Monadsなどを参照してください。

このようにDSLをF#の枠組みの中で作ることができる上に、優秀なHaskellianの方々が○○モナドを既に大量に生み出しているので、それを活用することができるのがモナドのすごいところです。

さあ、今日からモナドを使ってプログラミングしましょう!

補足:モナドって何よ?

普通にモナドを使ってプログラミングする上では問題になりませんが、モナドとなるにはモナド則を呼ばれる規則を厳密に満たす必要があります。

けっして、コンピュテーション式で使えるのがモナドだったり、ReturnとBindを実装したのがモナドではありません。

詳しくはモナドを作りたくなったら、調べるぐらいでいいと思います。