モナドを勉強してみたので、オブジェクトっぽいのを作ってみた。
社内勉強会でHaskellを細々とやっているんだけれど、やっとこさウワサのモナドの章が終了。使用している本はふつうのHaskellプログラミングです。
そこで、「モナドを使うと参照透明性を保って擬似的に状態をプログラミングできるっぽい」というモナドの気持がわかったような気がしたので書いてみる。これまであやふやなまま圏論を勉強したり、いろんなブログをよんだりしてきたけれど、それがほとんど繋った気がします。で勉強会でちょろっと作ったものが、本質をうつしだしてるわけじゃないけど、最初のとっつき段階ではいいんじゃないかなという例だったので書いてみます。
とりあえず、オブジェクト指向畑で育ってきた僕にとっては、「オブジェクト」は状態を保持している代名詞です。それをsetterをつかって状態をゴニョゴニョかえる感じ。なのでそれをモナドをつかって書いてみました。
まず、サンプル。Personクラス的なものをば。
data Person = P String Int --名前、年齢のつもり。 setName :: String -> Person -> Object Person setName str (P n a) = Obj (P str a) setAge :: Int -> Person -> Object Person setAge i (P n a) = Obj (P n i) getName :: Object Person -> String getName (Obj (P n a)) = n getAge :: Object Person -> Int getAge (Obj (P n a)) = a
こんな感じ。setterの返り値の型が「Object Person」となってるのは今は気にしないでおいてください。
そしていよいよモナドの定義です。こんな風に定義してみました。
data Object a = Obj a --「Object a」という型のデータ構成子(aは型変数) instance Monad Object where -- 「Object」はモナド。つまり「Object」は型構成子。 return x = Obj x -- return は ある型X(xの型)のデータを受け取って、 -- Object X型のデータを作る. (Obj x) >>= f = f x -- 演算子「>>=」は 左側に「Object X」型、右側に「X->Object Y」 -- をとって、「Object Y」型のデータを返す。
こんな感じ。 return でオブジェクト化して、>>=でメソッドを連鎖して適用するイメージ。
Person型のデータに対して(>>=)を定義してやればreturnはいちいち必要なくなるのですが、Objectモナドはすべての型に対してオブジェクト化、メソッドの適用的なことができる、抽象的な構造だというところが面白い。Javaで実装するならGenericsな感じ。で、main関数を
main = do let saito = return (P "" 0) >>= setName "Saito" >>= setAge 40 putStrLn $ getName saito print $ getAge $ saito >>= setAge 41
こんな風にしてみると、出力は
Saito 41
ここでやっているのは
- saitoという変数に
- 空のPersonを作って、setName "Saito"をして、setAge 40 したPersonオブジェクトを代入(する感じ。実はちがう。後述。)
- saitoから getNameして出力。
- saitoにsetAge 41したものを出力。
という操作。
通常「空のPersonを作って、setName "Saito"をして、setAge 40 する」というのを関数で書こうとすると、
saito= (setAge 40 (setName "Saito" (P " " 0))
という感じになるだろうか。数学に慣れていればいいが、手続き型のプログラミングに慣れしたしんだ人にとっては非常に気持わるいのではないだろうか。それを助けてくれているのが(>>=)演算子だ。演算子を連鎖させた場合を考えるとすぐわかると思う(定義は上を参照してほしい)。この演算子が左結合性をもっていることを注意して展開すると、
return x >>= f1 >>= f2 -- こんな風に手続き順に書くと、 ⇒ f1(x) >>= f2 ⇒ f2(f1(x)) -- ちゃんと関数の定義になってる。
と手続き風に順番にかくと、それがちゃんと関数になっているのがわかります。なのでこのObjectモナドを使えばこれまでと同じように、setter/getterおよび他のメソッドをもったデータに対して手続き風に書ける。ということですね。
ただし、(>>=)の制約上setterの返り値は「Object X」でないとだめなところがちょっとという感じです。不勉強なだけで多分標準で既に定義されてると思います。だって、「f: a-> b」という関数があったら、「return(f(x)」という関数を定義してやれば自動的に導出でるためです。実際、
(>>>=) :: Object a -> (a->b) -> Object b (Obj x) >>>= f = Obj (f x)
というのを作って(>>>=)で繋いでやれば、setterの戻り値はPersonで良くなります。getterはモナドから値を取り出す役目なのでこればっかりは、という感じですが。
ここでモナドってなんぞやという感じの質問に今答えるとすれば、「繋げる」ということでしょうか。
そしてもうひとつの鍵は「関数」を繋いでいるという事でしょうか。
もういちど、上のサンプルの一部をみてみましょう。
saito = return (P "" 0) >>= setName "Saito" >>= setAge 40
上のサンプルではこの文に対して、「代入する感じ」と説明しました。実は何がおこなわれているのかを見てみます。この文の右辺を定義に即して展開してみると
saito = setAge 40 ( setName "Saito" (Obj ( P "" 0)))
という関数を定義しているだけなのがわかります。処理を順次行っているかのように記述していたのですが、実際は関数定義をしていただけなのです。
このようにモナドを使うと、(>>=)で定義された規則にしたがって関数を「組み立てていく」ことができます。この考え方を膨らませると
状態遷移の関数f1,f2,f3,...を f1>>=f2>>=f3>==....と数珠つなぎにしておいて、それが順番に計算(評価)されていく時、それを状態とみなせますね。
もっといってしまえば、初期値からの状態遷移の計算をぜーーんぶ繋いでとっておけば、それで参照透明性を保持したまま状態として見ることができるというわけです。
今回のPersonの例では状態を保持している感があまりだせませんでしたが、次はちゃんと状態遷移関数を繋ぐStateMonadについて書こうとおもいます。
(最後に、今回作ったObjectモナドはHaskellに標準で組みこまれているIdentityモナドと等価なものです。)
今回使ったコード全体。
data Object a = Obj a instance Monad Object where return x = Obj x (Obj x) >>= f = f x data Person = P String Int setName :: String -> Person -> Object Person setName str (P n a) = Obj (P str a) setAge :: Int -> Person -> Object Person setAge i (P n a) = Obj (P n i) getName :: Object Person -> String getName (Obj (P n a)) = n getAge :: Object Person -> Int getAge (Obj (P n a)) = a main = do let saito = return (P "" 0) >>= setName "Saito" >>= setAge 40 putStrLn $ getName saito print $ getAge $ saito >>= setAge 41