Old/sampou.org/Programming_WayToHaskeller_てくてく

Programming_WayToHaskeller_てくてく

Programming:WayToHaskeller:てくてく


てくてく

(「てくてく」を編集する)|(Haskellerへの道)

= と <- と

なんか分かったとは言えないけどモナドって、実はそんなにムツカシイもんで もないのかもとか思っちゃいました。そのキッカケが=と<-です。ハイ。

モナドってのはどうやら何か秘密を隠しもってるヤツで、「秘密のある男性っ てステキ!」って感じのダンディーな方だったわけです。 通常関数型言語ではプログラムは式で構成されてて、そいつを評価すると値っ てやつを返してくれる訳ですね。 (通常って語るほど色々しってるわけじゃな いけどさ) で、実際大抵の場合大抵のプログラマにとって重要なのはこの値だっ たりするんですが、モナドの場合はアクションを取るものを返すようで、これ はこれで重要なんですが、実際に次なる(って言葉が曖昧だけど)処理にとって は大抵は役立たずだったりします。本当に欲しいのは隠し持ってる方の情報だっ たりするわけでんがな。

それを取り出すには x = getArgs とか x = getContents ではダメで、 x <- getArgsとかx <- getContentsなんですね。 当然次のようにして x = getArgsとか出来なくもないんだけど、

test = let x = getArgs
         in [x]

エバると、

Main> test
[<<IO action>>]
Main> 

全然嬉しくないわけです。えぇ。でも、

test2 x = do args <- head x
              return args

さらにこんな風にしてやれば、test2 testでもって内容を取り出せるっちゅー 寸法でんがな。 (あ、しまったreturnってことは…取り出せてねー) ま、まーだいたい意図は分るでしょ?ってことで、モナドに関しては、どうや らその区別がちゃんと分かっちゃうと案外可愛いものなのかもしれません。 つまり値を返してくるものとそいつが隠しもっているもの、そんでもって、そ れらの取り出し方ですな。

ただ、どうやら<-によってどんな風にその秘密を取り出せるかはそのモナドの 種類によって違うって感じでしょうか。つまりリストにおいてcarっていうか headで取りだすのすら、恐らく一つの方法にすぎないのかもしれません。 (つ まり機構そのものとしては)多値でもって全要素を返すような、Scheme?の receiveみたいなものだとしてもおっけー!なのかもしんないワケっすよ。

 -- あくまでキモチとしてこんな感じデス
 -- haskell
do (x, y) <- [1,2]
   ...
 -- scheme
receive (x y) ((lambda args (apply values args)) '(1 2))
   ...

こんなイメージですね。じゃあ何でリスト(モナド)では<-ってやった時に今の 仕様になってるのかっていうとワカリマヘンorz なんとなくモナド則とかいう屁理屈と関係あんじゃーねーの?って思ったんで すが、難しい議論は学生時代に足を洗ったのでパスです。

まーいずれにせよ、そう考えるとモナドってのは「なんでもモナド」(どっか で聞いたことあるな…)って言ってしまってもいいくらい、どこにでも出てき ていいんじゃねーの?とか思い上ってしまいそうです。

なんでもいいから何か新しい構造だとか型のようなもんを作ったら、それはリ スト同様に、値はその構造を返すし、その中身は<-とかで取り出すのが必然の ようにすら思えてきます。 (ヤバイ洗脳がすすんでるかも…)

あ、そうそう。ちょっと思ったのは、こんな感じですよ。少し前に別の場所で 書いたScheme?のfoldnってやつです。

(use srfi-1)
(use gauche.collection)
(define (foldn n proc . args)
  (let ((nils (take args n))
        (cols (map (cut coerce-to <list> <>) (drop args n))))
    (if (any null? cols)
        (apply values nils)
        (receive vals (apply proc (append (map car cols) nils))
          (apply foldn n proc (append vals (map cdr cols)))))))

さて、ここで書いた時から気になってたのは、carやcdrが出て来てるトコです。 これはあまりにもリストに寄った作りで、実際<collection>なものに対応する ために coerce-toを使っているのですが、「これはよくないなー、よくない よー」と山本直樹のマンガに出てくるキャラばりに思ったわけですよ。はい。 そこで本当ならイテレータ構築メソッドとやらを使って、nextだかgetだと かいった、一段抽象的なメソッドを使って書くべきなんでしょう。

エーっと、また何を書こうとしてたのか忘れたぞ。なんだっけ? あ、そーだ。つまり、これはcollectionだとかsequenceだとかってものに特化 してるんだけど、さらに一般化すると、中に何かを持っているものからそれな りの方法で中身を取り出すという、ただもうそれだけのようです。

正しくないんだけど、はっきしいって今の私にとっては、ほとんどScheme?の オブジェクトに対するrefとかslot-refとモナドにおける<-ってたいして変わ んねーじゃんって感じなわけです。少なくとも位置付けとしてはそんなもんだ ろうってアバウトに思ってても使えそうな気にはなっちゃってるわけです。思 い上りまくりです。えぇ。

まぁ実際には、おおーそうなのかー!!とか思う程簡単じゃないわけで、モナ ドのバヤイはそれを使うコンテキスト(?)みたいなのとモナド則を満すような >>=とreturnの組合わせってのがナゾを呼ぶわけですな。 そこに入りだすとなんか地獄が手ぐすね引いて待ってそうなので近寄りません がね。まぁ「臆病なくらいでちょうどいいのよね」とかつぶやいたりしましょ う。

さらに<-と>>=の関係が分ってしまえば天下か?

でもってさらに衝撃の事実ですが、

main = do cs <- getArgs
          mapM_ putStrLn cs

main = getArgs >>= (\cs -> mapM_ putStrLn cs)

とは同じなんだそうです。多分以前もどっかで誰かに教えてもらったのかもし れませんが、はっきりいって頭に入ってないっす。そんなに色々覚えらんねー よーって(無礼にも)思ったに違いありません。が、今なら分りますね。 モナド的にはどうも>>=とreturnってプリミティブっぽい気がするんですが、 個人的にはっきり言って <- のがずっとプリミティブって感じがするので、モ ナドって言わずに <- これを ref とでも名付けて>>=とかは、シンタックスシュ ガーというかなんというか、まぁ欲を言えばうやむやにしてくんないかなーと か思ったりします。 思いませんか?そうですか、私だけですか…

ちなみに、mapM_ ってやつですが、正直mapとかmapMとかmapM_とか、なんでこ んなしちめんどくせーのか知りませんが、まぁ型が違うのでこういうもんなん でしょうね。 emacsのミニバッファに表示される型を見ながらイロイロ試すか、マニュアル を検索するかしてしのぎます。

あらためて見ておきましょう。

main = do cs <- getArgs
          mapM_ putStrLn cs

こいつはgetArgsがIO [String]なので、アクションなんですが、[String]なヒ ミツを抱えてるワケですね。 でもって、<-でそのヒミツをなんとか取り出してcsに束縛するんです。そう、 <-はモサドみたいな機関で、モナドからモサドが国家機密を探り出すわけです。 (w (ただ、実際の動作はナゾです。そもそもこの時点で、ヒミツを全部取り出す 必要はないし、どういう順で取り出すのかもそのモナドの勝手じゃーんってこ とです。おそらくこれも必要になった時点で秘密がバクロされるという、ご都 合主義の2時間ドラマみたいな仕掛けになっているんでしょうね。 Preludeと いう名前といい、どうもHaskellを作った人は劇作家じゃないだろうかと勘繰っ たり) あとはmapM_で煮て焼いて食ってるだけっす。

main = getArgs >>= (\cs -> mapM_ putStrLn cs)

こいつのバヤイも同じです。getArgsが抱えてるヒミツを>>=を通して右に送り つけてる感じでしょうか。右に渡ってるのは見えないけど[String]なデータだ と思えばよく、それをlambdaが処理してIO ()な形で始末してるんです。

私が>>=より<-の方がプリミティブっぽいと感じたのは、多分データの受け渡 しが見えるからのような気がします。ハイレベルの言語って結局同じことをや るんだけど、暗黙裡にやってくれることが多いっていうか広いっていうか、まぁ いい感じに処理してくれちゃうわけで、見えない部分で活躍する裏方さんが大 勢いらっしゃるんです。 >>=もある意味そうなんですよね。今はlambdaを書い ているからcsが見えてますが、もし、

printArgs = mapM_ putStrLn

こんな風になってたら、

main = getArgs >>= printArgs

csが消滅しちゃうわけですね。そーすっと、csの存在が見えないのに動作の裏 側で暗躍することになるので、説明された時にイメージできないわけです。 (という言い訳です) 逆に<-を使うとイヤでもcsの存在を書かなきゃならないので、これは表社会の 住人さんだからなんか人に優しい感じがするんですよね。(という言い訳です よ、しつこいけど)

getContentsのバヤイ

ほとんど同じなんだけど、getContentsを見ておきましょう。

main = do cs <- getContents
          putStr cs

getContentsはIO Stringってことで、今度はヒミツはStringなものなので、 mapM_とかせずにそのままputStrに渡すことにしました。なんか全然簡単って 気がしてきましたね。

さらにさらに<-ってのはここにも登場?

でもって実は怪しい集会に潜入ルポって来た成果として、次のようなのも。

[(x,y) | x<-[1..100], y<-[x..100],y==x*2]

奥さんこれです。 リスト内包表記ってやつね。ここで出てくる<-ってのはまさにモサドです。ハ イ。モナドじゃなくモサドの方ね。すごくないっすか?恐らく逆で、これは数 学の表記をもとに<-な記号を採用して、その後一般化されたんじゃねーのかなー と思ったりするんですが、まさにリストもモナドなわけです。実際これ見たら、 やらずにはいられません。

Main> [1,2,3]>>=(\x->(x,x*x))
ERROR - Type error in application
*** Expression     : [1,2,3] >>= (\x -> (x,x * x))
*** Term           : [1,2,3]
*** Type           : [b]
*** Does not match : (a,a)

あら?あーあーあーそうか。

Main> [1,2,3]>>=(\x->[] (x,x*x))
ERROR - Type error in application
*** Expression     : [] (x,x * x)
*** Term           : []
*** Type           : [c]
*** Does not match : a -> b

え?えーっとリストにすればいーんだよね。も少し簡単なので確認。

Main> [1,2,3]>>=(\x->[] x)
ERROR - Type error in application
*** Expression     : [] x
*** Term           : []
*** Type           : [c]
*** Does not match : a -> b

あうっ!そもそも間違ってんのか。

Main> [1,2,3]>>=(\x->x:[x*x])
[1,1,2,4,3,9]

おっけー!ちょっと意図が違うけど。

Main> [1,2,3]>>=(\x->[(x,x*x)])
[(1,1),(2,4),(3,9)]

あ、これがやりたかったの。いーんでないすかね。

Main> [1,2,3]>>=(\x-> return (x,x*x))
[(1,1),(2,4),(3,9)]

勿論こんな風にreturnで[a]なリストモナドを返すのも正しい。ってか >>=と コンビで使うならこっちのが推奨なんでしょう、きっと。 ただ、[]を使うのは、いわば(上に示した)carやcdrを使ったfoldnの定義と同 じで、リストに特化した書き方になってる。一方でreturnを使う書き方はより 一般化された書き方になってるわけさ。

もう少し頑張ってみます。

Main> [1..100]>>=(\x-> return [(x,y) | y<-[x..100], y==x*2])
[[(1,2)],[(2,4)],[(3,6)],[(4,8)],...[],[],[],[]]

あらら。おしい!所望の結果は

Main> [(x,y) | x<-[1..100], y<-[x..100],y==x*2]
[(1,2),(2,4),(3,6),(4,8),...,(47,94),(48,96),(49,98),(50,100)]

なので、一個リストの構造が余計だなぁ。えーっと、あー分りましたよ!

Main> [1..100]>>=(\x-> [(x,y) | y<-[x..100], y==x*2])
[(1,2),(2,4),(3,6),(4,8),....,(47,94),(48,96),(49,98),(50,100)]

キターーーーーーーー! ぃよーーーっし、じゃあ続いてやっちゃうよ!

Main> [1..100]>>=(\x-> [(x,y) | [x..100]>>=(\y-> [y | y==x*2])])
ERROR - Undefined variable "y"

あぅ!なんでダメなのさ?落ち付け落ち付け。

Main> [1..10]>>=(\x->[x])
[1,2,3,4,5,6,7,8,9,10]

いけるよね。じゃあ絞ろう。

Main> [1..10]>>=(\x->[x| x==3])
[3]

うーん。いけるな。ってことは、[x..100]が駄目なのかなぁ。

Main> let x=10 in [x..100]
[10,11,12,13,14,....,95,96,97,98,99,100]

勿論大丈夫だよね。うーん。えーっと、あらら?そうか。よくよく見てみたら、 [x..100]>>=(\y->[y| y==x*2])ってしたら、それは単なるリストだからそれが yだってのは結びつかないのか。

Main> let x=5 in [y | [10..100], y==x*2]
ERROR - Undefined variable "y"

みたいな感じになっちゃってんだな。そうか、そりゃダメですわ。 マジに副作用ないの?

この数日なんだかモナドについてまた考えてたんだけど、またちょっと目覚めたかも。

よく言われることなんだけど、Haskellって副作用がないって本当?とか 副作 用がなきゃプログラム書けないじゃんとかそーゆーやつです。 色んなところを調べると、モナドに閉じこめたとかモナドがそういう汚い世界 のことを一手に引き受けてくれてるとか書いてあるんだけど、相手からすると、 なーんだ結局あるんじゃん、副作用って言われちゃうわけですね。

main = do cs <- getContents
          putStr cs

こちらにあるコードをGHCを使ってコンパイルします。

[email protected]> ghci cat.hs -o cat

そうするとcatっていう実行可能コードが出来ます。こいつを使ってみます。

[email protected]> ./cat
hoge
hoge
fuga
fuga
うひゃ
うひゃ^D
[email protected]> 

これを見ると、readする度に違う値を返してきてるじゃん。って思うわけです。 同じ式が評価する度に違う値を返してきてるってのは副作用があるってことじゃ ん? BUT!!そりゃ早トチリってもんですね。よーくコードを見ると、どこにもread なんてないんです。勝手にreadしてるって妄想しちゃイヤーンです。

つまり、一行読んでその文字列を返すなんて関数は使われてないんですよ。 getContentsってのはreadじゃないってのがとっても重要。

getContentsは誤解を恐れずに言うならば、 私が入力する“hoge”、“fuga”そし て“うひゃ”っていう3行 (もしかしたらそれ以降にも入力されたかもしれない 行)全てを知ってて、 そいつを一種のリストのようなものに入れて返してきて いるんです。 そして、必要になったところで順番に中から取りだして表示しているに過ぎな いんです。 え?だって“うひゃ”ってのを読み込む前に“hoge”を表示してる(putStrしてる) じゃん!? そりゃそうですYo!だって遅延評価ってそういうもんじゃん。それが許されな きゃ、[1..]なんて無限リストを全部構築しなきゃなんも計算できなくなりま す。 cat.hsでは、“hoge”を表示するときには“うりゃ”が本当に必要になるわ けではないから、べつにアクセスしてないだけなんですね。嘘だと思うなら、 [1..]をHugsのプロンプトに入れてみればいいでしょう。

Main> [1..]
[1,2,3,4,5,6,7,8,9,10,11.............................

おっとっと、すぐ^Cしなきゃじゃんじゃんでちゃいます。ここでは無限大まで のリストを構築しなくても(つーかそんなんできないけど)、最初の方の表示に はなんの問題もないからちゃんと表示できてますよね。もー一個いっときましょ う。

Main> take 5 [1..]
[1,2,3,4,5]
Main> 

この計算がちゃんと終了するのと同じ理由ですね。 そうなんだよね〜。リストもモナドだって衝撃を受けた時に気付くべきでした。

listA = ["hoge", "fuga", "うひゃ"]

っていうリストから順にデータを取りだした時に、「listAはアクセスする度 に、“hoge”とか“fuga”とか“うひゃ”とか毎回違う値を返してくるから副作用が ある」なんて言う人はおらんでしょう。それと同じことだったんですね。

ようやく結論ですが、副作用ってのを本当にうまく消してしまってるんですよ。 副作用はキレイサッパリ消失してしまったんです。押し付けられた一部の関数 が副作用を引き受けたんじゃなくて本当になくしたんです。にも関らず、副作 用と同じことを実現してるんです。

副作用みたいに毎回違う値が欲しいなら、その毎回変るであろう値ぜーんぶを 持つ一つの値(構造)をでっちあげて、そこから順番にデータを取り出すような 仕組み(モナディックなプログラミング)を作り上げるわけです。

まー考え方を変えただけって言われるかもしれませんが、上のようにリストに なぞらえて理解すると本当に消えちゃったでしょ?副作用。それでも言い換え ただけって思います?実際そういう仕組みを作ってコードもそーゆー感じに書 いてますってところが徹底してると思いますね。少なくとも私は自分で自分の 説明に納得しちゃたんでもういいです。はい。(^^)

最初はそんな副作用を使わずに入出力を表現するなんて無理だよーって思って ましたが、あなどれませんHaskellでもってモナド、そしてなにより本質的に それを可能にした遅延評価の世界です。 lazyだからこそ、そういう考え方が 成立するし、そういう意味付けと矛盾しない動作が実現できるんですね。

(「てくてく」を編集する)|(Haskellerへの道)


Last modified : 2006/06/12 12:53:49 JST