[Haskell] Monad周りで自分なりに考えてみたこと

[Haskell] Monad周りで自分なりに考えてみたこと

Haskellを真面目に勉強し始めて10日ほど経つ。一応、Yet Another Haskell Tutorialをだいたい読み終わったわけだが、最後のモナドの章だけは断念した。日本語で何回説明されてもわからないものを英語で説明されてもわかるわけがない。

とはいえ、一応Haskellの文法などは勉強してみて、モナドについてもいろいろと聞きかじってみたわけで、自分なりに「モナドってこんなものなのかな?」っていうようなものはつかめたような気がする。なので、自分なりの理解やら疑問やらをメモっておこうかと思う。

自分の考えの整理のために書き連ねるので、もし間違っていたり考え違いをしているところがあれば (そんなところばっかだろうけど) 、容赦なく突っ込んでもらいたい。

Monadって、「計算を合成する」っていう概念とか発想のこと?

モナドを勉強し始めると、MaybeモナドやらIOモナドやらStateモナドやらがたくさん出てきて訳がわからなくなる。そんで、「結局モナドって何なの?」っていう疑問が当然沸くわけだが、多くの場合「計算を合成すること」っていう答えしか返ってこなくて、余計に訳がわからなくなる。

だけど、ある時、今まで僕がモナドを理解できなかった理由は、「つまり、モナドで何ができるの?」っていう疑問に縛られすぎていたことなのかな、と少し思った。モナドっていうのは、「計算を合成する」っていうアプローチのことで、そのアプローチを使ってどのような問題を解決できるかはモナドの使い方による、っていう理解でいいのかな?

例えば、Maybeモナドなら値を返さない可能性がある関数をエレガントに記述できたり、IOモナドなら副作用のある処理をIO型に封じ込めたりすることができる。今までは、MaybeモナドにできることとIOモナドにできることとの共通点がモナドの本質だったりするのかもしれないと思っていた。でも、実はMaybeモナドの目的とIOモナドの目的っていうのは全然別のもので、全く別の目的がたまたま「計算を合成する」っていうアプローチでエレガントに解決できただけっていうことなのかな?

このように理解してみると、MaybeモナドとかStateモナドとかIOモナドとかが、一見全く別の処理をしているように思えても、さほど気にならなくなったし、「この処理するのにわざわざreturnを定義したりする必要なくね?ってか、わざわざモナド使わなくても普通に処理できるんじゃね?」とか思ったときも、「この処理をするのにモナドの全ての機能は必要ないけれど、モナドっていうHaskellの標準的なアプローチに則って処理を書いたんだな」と考えられるようになった。

この理解が正しいのかはわからないから、もし間違っていたら指摘してもらいたい。現状では、まだStateモナドとかが全く理解できていないから、まだ理解していないモナドを理解したら考え方がいろいろと変わるのかも。

Monadで順序処理が記述できるのは...だから?

ここからは全く別の話。Haskellの遅延評価の仕組みがよくわからなくなったので、自分なりの理解を書いておきたいだけ。

以前、「getLineって、わざわざIOモナドを使ってIO String型を返さなくても、普通にString型を返してしまえばいいんじゃね?」と考えたことがある。もちろん、IOモナドには副作用のある処理を他の関数に波及させない効果があるから、IOモナドを使ったほうが便利なわけだけど、もし副作用が他の関数にも波及することが我慢できるのならば、別にIOモナドを使わなくてもいいんじゃね?とか考えたわけ。

でも、実際のところモナドには順序処理を記述する機能があるらしいので、それについて少し考えてみたら、以下のような理解に至った。

仮に、getLineがIOモナドを使わずにString型を返すとすると、↓のような書き方ができる。

oneFunction getLine

このように書くと、遅延評価言語であるHaskellでは、getLineが実際に実行されるかどうかはoneFunctionの定義次第になる。つまり、oneFunction内で第一引数が評価されなければgetLineは実行されないし、いつ評価されるかもわからない。一方、IOモナドを使ってgetLineIO String型を返すようにすると、↓のような書き方になる。

getLine >>= anotherFunction

ここで、(>>=)は、IO String型を受け取ってanotherFunctionStringを渡す。oneFunctionのときと同様に、anotherFunctionが渡されたStringを実際に評価する保証はないし、どのタイミングで評価されるかもわからない。でも、(>>=)IO String型を使用することは保証されているから、anotherFunctionが実行される前に必ずgetLineは実行される。だから、モナドを使うと順序処理を自然に記述することができる、という理解で正しいのかな?

一般化すると、a という型を返す処理を、モナドを使わずにそのまま他の関数に引数として渡すと、その処理がいつ実行されるかを保証することができない。一方、m a という型を返す処理を、(>>=)を通じて他の関数に渡す場合、(>>=)の中で m a が評価されることが保証されていれば、確実に(>>=)の中でその処理が実行されることが保証できる、ということ?

大切なのは、m a(>>=)の中で評価されるんだったら、たとえ(>>=)の右辺に渡される関数で a が評価されなくても、(>>=)の左辺の処理は実行されるということか?

a が評価されなくとも m a が評価されるのなら左辺の処理が実行されるというのは不思議な気もするけど、遅延評価言語でもモナドを使えば順序実行ができる理由は、以上のような理解で正しいのかな?

Haskellの型推論って返り値の型も参照するのかな?

ここからも全く別の話。というか、たまたまreturn関数を見ていたときに疑問に思っただけで、そもそもモナドとの関係もない。

例えば、↓のような関数を実行してみる。Hugsで実行すると、ちゃんとsomethingっていう文字列が出力される。

(>>=) (return "something") putStr

僕が疑問に思ったのは、どの型に属するreturnメソッドが実行されたのかな?という点。returnの型は、return :: Monad m => b -> m bだから、引数を与えてもどの型に属するreturnメソッドをディスパッチするかは決めることができない。実際、Hugsでreturnを単独で実行すると↓みたいなエラーになるし。

Hugs> return "something" / ERROR - Unresolved overloading / *** Type       : Monad a => a [Char] / *** Expression : return "something"

でも、

(>>=) (return "something") putStr

のように呼び出せば、putStrIO ()を返すから、(>>=)がIOモナドを要求することがわかって、returnもIOモナドを返すようになっているっぽい。つまり、Haskellは返り値の型からも型推論をしているっていう理解でいいのかな?

と、ここまで書いてみて、ちょっと違うような気もしてきた。(return "something" >>=)の型 (Monad m => (String -> m b) -> m b) とか、(>>= putStr)の型 (IO String -> IO ()) とかを見てると、遅延評価の仕組みとかできるだけ抽象的な型を推論する仕組みとかを考えれば当たり前の挙動のようにも思えてきた。返り値の型を見ているような気もするけど。

とりあえず、もう少し考えたほうがよさそうだ。

まとめ

Haskellは難しい><

スポンサーサイト

関連記事

トラックバック URL

http://liosk.blog103.fc2.com/tb.php/137-85768712

トラックバック

コメント

コメントの投稿

お名前
コメント
編集キー