Clojureでモナド(Part3)

前置き

Part3では、独自のモナドを定義してみます。

おさらい

前のPart

登場人物

v
value。普通の値。mv(monadic value)と区別するため、normal valueなどと呼ばれることもある。
mv
monadic value。モナドコンテキストをまとった値。
mf
monadic function。vを引数にとり、mvを返すタイプの関数すべて。
m-result
mfの一種。引数vに対して、vを表現する最も単純なmvを返す。unitと呼ばれることもある。
m-bind
mvとmfを引数にとり、別のmvを返す関数。flatmapと呼ばれることもある。

モナドとは

  • 型引数を持った抽象型である
  • 単なる型にとどまらず、プラスαの味付けが加わる
  • その味付けは、m-resultとm-bindをどう実装するかによって決まる

モナドの掟

独自のモナドを定義するために必要なのは、m-resultとm-bindの実装です。それぞれのシグニチャ(引数と戻り値の仕様)は決まっているので、それに準じて実装します。シグニチャを守れば中身は自由かというと、そうでもありません。まともなモナドとして振る舞うには、以下の公理を満たすように実装する必要があります。

(m-bind
  (m-result v)      ≡      (mf v)
  mf)
(m-bind
  mv                ≡      mv
  m-result)
(m-bind                     (m-bind
  (m-bind mv mf1)   ≡        mv
  mf2)                        (fn [v] (m-bind (mf1 v) mf2)))

Learn You a Haskell for Great Good!やWikipediaによれば、仮に、2つのmfを合成する関数 >=> が定義できるとすると、上記の3つは以下のように書き直せるそうです。

(>=> mf m-result) ≡ mf
(>=> m-result mf) ≡ mf
(>=> mf1 (>=> mf2 mf3)) ≡ (>=> (>=> mf1 mf2) mf3)

つまり、m-resultは単位元で、mfの合成には結合則が成り立つってことです。

ちなみに、>=> の定義は、こんな感じです(defmonadfnについては後述します)。

(defmonadfn >=>
  [mf1 mf2]
  (fn [v]
    (m-bind (mf2 v) mf1)))
(defn f [v] (list v (- v)))
(defn g [v] (list (inc v) (dec v)))
(defn h [v] (list (* v 2)))

((with-monad sequence-m (>=> f m-result)) 3) ;=> (3 -3)
(f 3)                                        ;=> (3 -3)
((with-monad sequence-m (>=> m-result f)) 3) ;=> (3 -3)


((with-monad sequence-m (>=> f (>=> g h))) 3)  ;=> (7 -7 5 -5)
((with-monad sequence-m (>=> (>=> f g) h)) 3)  ;=> (7 -7 5 -5)

独自のモナド

普通、自然数は0, 1, 2, ...と書きますが、他の方法で自然数を表記することもできます。例えば…。

  • ゼロは空集合{}
  • ある自然数nの次の自然数は、nだけを要素とする集合

この定義によれば、0は{}、1は { {} }、2は { { {} } } となります。このように表記する自然数を仮にペアノ数と呼ぶことにしましょう(造語です)。

そして、これを表現するモナドを定義してみます。

  • vは自然数
  • mvはペアノ数を文字列にしたもの
  • m-resultは、vのペアノ数表記を返す

モナドを定義するには、clojure.algo.monadsが提供するdefmonadマクロに、m-resultとm-bindの定義を与えればOKです。

(defmonad peano-m
  [m-result (fn [v]
              (loop [n v p "{}"]
                (if (zero? n) p
                  (recur (dec n) (str \{ p \})))))
   m-bind (fn [mv mf]
            (let [v (dec (/ (count mv) 2))]
              (mf v)))])

使ってみます。

(with-monad peano-m
  (m-result 0))             ;=> {}

(with-monad peano-m
  (m-result 4))             ;=> {{{{{}}}}}

(domonad peano-m
  [a "{}"
   b "{{}}"
   c (m-result (+ a b 2))]
  (* b c))                  ;=> {{{{}}}} (つまり3)

オキテを満たしているかどうかは、良く分かりませんが…。

(defmonadfn f [v] (m-result (* v 2)))
(defmonadfn g [v] (m-result (- v 2)))
(defmonadfn h [v] (m-result (inc v)))

((with-monad peano-m (>=> f m-result)) 3) ;=> {{{{{{{}}}}}}}  (6)
(with-monad peano-m (f 3))                ;=> {{{{{{{}}}}}}}
((with-monad peano-m (>=> m-result f)) 3) ;=> {{{{{{{}}}}}}}

((with-monad peano-m (>=> f (>=> g h))) 3)  ;=> {{{{{}}}}}  (4)
((with-monad peano-m (>=> (>=> f g) h)) 3)  ;=> {{{{{}}}}}

m-zeroとm-plus

モナドの性質を決めるのはm-resultとm-bindですが、オプション的に使われる属性としてm-zeroとm-plusがあります。

m-zero
特殊なmv値で、「vがない」を示す。
m-plus
任意の数のmvを引数にとり、それらをまとめたmvを返す関数。

すべてのモナドにm-zeroとm-plusが定義できるわけではありません。例えば、maybe-mのm-zeroはnil、sequence-mのm-zeroは空シーケンスですが、identity-mやstate-mにはm-zeroに相当する値はありません。またm-plusについて言えば、sequence-mのm-plusは全てのmvをconcatする関数になります。maybe-mの2つのmv値を「まとめる」というのは不自然な気もしますが、実際は以下のように実装されています。

maybe-mのm-plus
(fn [& mvs]
  (first (drop-while nil? mvs)))

mvsの中で、最初に見つかった非nilな値を返すってことですね。

peano-mの場合はどうでしょうか? m-zeroは "" で良さそうですが、m-plusは定義できないと思います。"plus"という名前からは「足す」を連想しますが、m-plusの "plus" はもっと抽象的な、「まとめる」とか「あわせる」くらいの意味でしょう。

「m-zeroは""」を採用して、peano-mを定義しなおしてみます。ついでに、m-zeroを踏まえてm-resultとm-bindの実装も修正します。

(defmonad peano-m
  [m-zero ""
   m-result (fn [v]
              (if (neg? v) m-zero
                (loop [n v p "{}"]
                  (if (zero? n) p
                    (recur (dec n) (str \{ p \}))))))
   m-bind (fn [mv mf]
            (if (= mv m-zero) m-zero
              (let [v (dec (/ (count mv) 2))]
                (mf v))))])

m-resultの引数が負値ならm-zeroを返すようにしました。またm-bindの引数に渡されたmvがm-zeroなら、maybe-mのように即終了します。

(with-monad peano-m
  (m-result -1))   ;=> ""

(domonad peano-m
  [a "{}"
   b "{{}}"
   c (m-result (- a b))]
  c)               ;=> ""

小道具たち

最後に、clojure.algo.monadsライブラリが提供するヘルパー関数などをいくつか紹介します。

条件付きモナド内包

モナド内包のローカルバインド部には条件を付けることができます。

(domonad sequence-m
  [a (range 5)
   :when (odd? a)]
  (* 2 a))          ;=> (2 6)

このフォームは以下のようにマクロ展開されます。

(with-monad sequence-m
  (m-bind
    (range 5)
    (fn [a]                 ;★1
      (if (odd? a)
        (m-result (* 2 a))
        m-zero))))

m-bindの引数に指定される無名関数(★1)は、mvから取り出したnormal valueが偶数の場合はm-zeroを返すようになっています。sequence-mの場合、m-zeroは空シーケンスです。

またm-bindの実装は以下のようなものでした。

;;m-bind
(fn [mv mf]
  (apply concat
    (map mf mv)))

このmfが★1です。mvは (0 1 2 3 4) です。よって、()、(2)、()、(6)、() をconcatしたものが最終結果になります。maybe-mのようにm-zeroになったら即終了、というわけではないので注意して下さい。

defmonadfnマクロ

通常、m-result、m-bind、m-zero、及びm-plusは、with-monadフォームの中で使います。with-monadマクロの第1引数にモナド名が指定されて初めて、m-resultやm-bindなどの実体が決まるからです。

しかし、使うモナドの決定を先延ばしにしたいこともあります。例えば、前述の >=> という関数の実装は、すべてのモナドに共通なので、関数定義の時点では、with-monadを使いたくありません。このようなニーズに応えるのがdefmonadfnマクロです。

2つのmfを合成する関数
(defmonadfn >=>
  [mf1 mf2]
  (fn [v]
    (m-bind (mf2 v) mf1)))

defmonadfnで定義した関数を呼ぶときは、with-monadフォームの中に入れます。

(defn f [v] (list v (- v)))

((with-monad sequence-m
   (>=> f m-result)) 3)   ;=> (3 -3)

m-liftマクロ

m-liftマクロは、normal valueを引数にとりnormal valueを返す関数を元に、monadic valueを引数にとりmonadic valueを返す関数を生成して返します。

例えば、ペアノ数における足し算を行う関数を定義したいとしましょう。

(defn peano-m-plus
  [mv1 mv2]
  (domonad peano-m
    [v1 mv1
     v2 mv2]
    (+ v1 v2)))

(peano-m-plus "{{{{}}}}" "{{}}")   ;=> {{{{{}}}}}

これでも良いのですが、m-liftを使えばもっと簡単に定義できます。かけ算で試してみましょう。

(with-monad peano-m
  (def peano-m-multi (m-lift 2 *)))

(peano-m-multi "{{{{}}}}" "{{{{}}}}")  ;=> {{{{{{{{{{}}}}}}}}}}

m-liftの第1引数には、生成したい関数の引数の数を指定して下さい。

m-chain関数

m-chainは、複数のmfのシーケンスを引数にとり、それらを連続してm-bindする1つのmfに合成して返す関数です。

(with-monad maybe-m
  ((m-chain [#(map inc %)
             #(filter even? %)
             #(filter neg? %)
             first]) '(5 -2 1 4 -7 9 2)))   ;=> -6

前段のm-bindが返すmv値が、後段のm-bindの第1引数に指定されます。Clojureの -> マクロに似てますね。

トランスフォーマ

トランスフォーマを使うと、2つのモナドを組み合わせて新しいモナドを作ることができます。

例えば、sequence-mとmaybe-mを組み合わせてみましょう。sequence-mの個々のvが、実はmaybe-mのmvになっているようなケースです。この場合、トランスフォーマmaybe-tを使ってsequence-mを変身させます。

(def maybe-in-sequence-m (maybe-t sequence-m))

(with-monad maybe-in-sequence-m
  (m-result 1))                  ;=> (1)
(with-monad maybe-in-sequence-m
  (m-result nil))                ;=> (nil)
(domonad maybe-in-sequence-m
  [a [1 nil 8 9]
   b [nil 3 5]]
  (* a b))                       ;=> (nil 3 5 nil nil 24 40 nil 27 45)

sequence-mの個々のvのレベルでmaybe-mの効果が適用されます。よって最後のフォームでは、normal valueのaとしてnilを取り出した時点で、bのバインドがスキップされて、(nil) がその周回のmvになります。その時点で全体が終わるわけではなく、aの続き(つまり8と9)が実施されます。

maybe-tでトランスフォームするとき、「失敗」を意味する値としてnilを使いたくない場合もあるでしょう。そのときは、maybe-tの第2引数に「失敗値」を指定すればOKです。

(def maybe-in-sequence-m (maybe-t sequence-m :nothing))

(domonad maybe-in-sequence-m
  [a [1 :nothing 8 9]
   b [:nothing 3 5]]
  (* a b))    ;=> (:nothing 3 5 :nothing :nothing 24 40 :nothing 27 45)

順番を逆にして、maybe-mの個々のvがsequence-mのmvになるパターンを考えることも可能ですが、あまり意味は無いでしょう。なぜなら、sequence-mのmvがnilになることはあり得ないからです。

結び

3部構成でClojureのモナドを解説してきましたが、本音を言えば、この程度ではモナドを理解したとは言えません。今の自分としては、これが精いっぱいです。自在に使いこなせる道具には全然なってません。

Last modified:2014/10/31 17:30:37
Keyword(s):
References:[Clojureでモナド(Part2)] [FP: 関数型プログラミング]
This page is frozen.