Island Life

< Y Combinator portfolio | 求職中マーク >

2007/07/29

どう書く.org, Haskell

どう書く.orgは、問題の大きさが ちょろっと書いてみるのにちょうどいいくらい (SchemeやHaskellで 「エディタ1画面に収まるくらい」の長さ) なこともあって、ついつい考えてしまう。 締切前なのに。

せっかくなのでHaskellの練習をしてみてるんだけど、このくらいの小さな 問題だと確かに型って全然考えないなあ。全部型推論任せ。便利だわ。 Schemeにも型推論欲しい。

遅延評価については、まだ頭の中で動作を展開している感じだなあ。 Haskellの関数やオペレータを「遅延評価つき高レベルライブラリ」みたいに 認識している感じで、組み合わせた時の動作は操作的に理解している。 ただ、それだとモナドを組み合わせてくようなところで途中でわからなくなるんで、 宣言的頭をもすこし強化せねば。

そんで、Haskellをぽちぽち書いてみての印象:

  • 優先順位覚えるのが面倒。型エラーのほとんどは今のところ優先順位を 間違えてて思ってたのと違う結合になっちゃってるせいだ。慣れればいいんだろうけど 慣れちゃうのも癪なんだよなあ。
  • ごちゃごちゃいじってる最中に、ソースが型的にinconsistentになるってことが 良くある。例えば下請け関数x,y,zを書いてghciで動作を確かめて、それを組み合わせた wを書いている途中でふと気づいてxの定義を直した、みたいな場合。 またさくっとソースをロードしてxの動作をインタラクティブに確かめたいんだけど、 xの型が変わってるのでwのコンパイル時にエラーになる。 Lisp/Schemeだとそこは全く気にしないでいいわけだが、Haskellerはどうするんだろう。 いまのところこういうケースではwの定義をいちいちコメントアウトして ロードしてる。

Tags: Programming, Haskell

Past comment(s)

nobsun :

関数適用は左結合で最強、$ は右結合で最弱、これだけ覚えておけば、あとは括弧でくくればいいと思うだす

nobsun :

具体的にはどういう場合なのかしらん。w の型宣言を省略するか、しないかということ?Schemeで気にしなくていいところは、Haskellでも気にしないんじゃないかなぁ。

cut-sea (2007/07/30 09:36:47):

型宣言がどうこうじゃないと思う。xの型が変わってしまった時にwもそれに合うように修正してやらないとダメで、新しくしたxだけを評価して確認することができない。 新しくなったxの確認のためにwも刷新してやらないといけないってことだと思う。

;; 正直型はどうでもいい
x :: Int -> Int
y :: a -> a
z :: [a] -> Int
;; ここでは引数にx,y,zを使ってるっぽく書いてるけど、
;; 一般化して x,y,zが使われている場合と考えてね。
w :: (Int->Int) -> (a->a) -> ([a]->Int) -> a

な何かだったとして、

;; 新しくxを書き換え。これもさっきと型が違えばなんでもいい。意味なし。
x :: [(a,Int)] -> (a, Int)

となる何かに変更したとする。 xだけちょっと確認したいんですよ。要は。 この時にwをどうしてる?ってことでしょう

cut-sea (2007/07/30 09:54:36):

で、wがfとかgから使われてる場合もあるので、コメントアウトは芋蔓的に拡大しうるからあまり常用できそうにないっすよね。 多分、xを直接変更するんじゃなくてx'を別途作って、動作確認してからxと差し替えたりするんじゃないかなぁ。まぁその辺はどうしても手間にならざるを得ないと思うけど。

nobsun :

この手間は言語に関係なく同じだよね。

cut-sea (2007/07/30 23:38:36):

もちろん修正の範囲は言語に関係なく同じです。
Haskellでは、この場合コメントアウトなんてしないよ。 どうせxだけ確認してもダメでしょ? 結局最後には全部修正しないとダメなんだから、 xの確認なんかより全体がロードできるようにするのが先! ということであれば、まぁそういうものかと納得するしかないです。
ただSchemeだと開発の作業順(作業量じゃなく)て、 repl使ってちょこちょこ部分部分を積み上げていけるスタイルが、 楽チンだと感じる面があるので、 作業量が一緒だから同じでしょということに対しては、 やっぱり感覚が違うなーとは思いますが。

Shiro (2007/07/30 12:02:59):

そういうことです > cut-sea。 ファイルをリロードしたいんだけど、その中の「すぐに使わないことがわかってる関数」 の中に矛盾があった時どうするかっていう。 Schemeではそれは気にしなくていい。

nobsun :

Haskeller は怠けものだから、そもそも「すぐに使わないことがわかってる関数」なんか書かないんです。:)

cut-sea (2007/07/30 23:02:51):

ハイそうですかとは納得できない。:(
nobsunや他のHaskellerが書く書かないじゃなくて、 仕様変更が関数の型を変えることを要求するってのはあるんじゃない? リファクタリングの過程でもいいけど。
Haskellはじゃなくて、Haskellerは人間なんだから、 間違った型の設計をすることはあるんじゃない? それに気付くのは後の方だったりしないのかな?
この件については、私自身はトレードオフだと思ってて、 Haskellは(型レベルでの)全体のconsistencyを保証することと引き替えに、 プログラマにはコード全体を完成させることを要求するわけで、 部分だけ試したいという欲求にはそもそも応えにくいのは仕方ないと思ってるんですよ。 Schemeとかはconsistencyの確保はプログラマまかせだけど、 全体と矛盾している状況でもとりあえず部分を確認することは出来る。
確かにアプリが完成した(エラーが出なくなった)時点でHaskellの方が安心感はあると思う。 でも、改修規模が大きな状況では、改修の途中段階で、ある変更したことによって、 その部分がとりあえず動くという確認がこまめに取れるのはストレス軽減になるでしょう?
だからHaskellでだって一個関数書いたらC-cC-lでこまめに確認したりしますよね? これって今書いたところまでが問題なさげか確認して足場を確保してる感じだと思う。
セーブポイントのない巨大なダンジョンの中を延々歩かされるストレスから逃げたいんですよ。 セーブポイントはいたる所にある方が安心できるんです。
(Haskellerはリリースした後までinconsistentかもなんて 不安やストレスから逃げたいんですよ。って言われたら納得ですが。)

で、このセーブポイントがHaskellだとなんかコメントアウトしたりして、 面倒なんだけど、どうしてんのかな?って のは当然の疑問だと思う。 言語の問題じゃないんだから、そういう状況は当然ありえると思うんだけどなぁ。

nobsun :

Haskellerにとって一番重要なのはリリースした後までinconsistentかもなんて不安やストレスから逃げたいということです.

cut-sea :

ナットク。:)

nobsun :

やっぱり考えかたが違うかもしれない.そもそも使われかたによって,型が決まるので,x :: a だったものを x :: (a, Int) にするということの動機あるいは根拠はこれを利用する関数側からのものだから.孤立した文脈でテストしたいなら,x' という名前でテストするなりすればいいんじゃないかと思うけど.ghci にかぎれば,repl で let x = ... とするというのもありかも.

nobsun :

x を直接利用している関数が w だけだということが分っているなら

w = ... x ... x ....

w = undefined {- ... x ... x ... -}

のように一時的に undefined の値にしてしまうという手はあります. 一種の comment out だけど、w 自身は型としては適切に定義されるので w を呼ぶ関数は変更しなくてもよい。ときどきやります。

jmuk :

ああ nobsun に書かれてしまったかな。 w の局所定義で x を undefined にするという手があると思います。というのを書いてみました→ http://www.jmuk.org/diary/2007/07/31/0 これは w でしか x を利用していない、といったケースを想定しています。 w 自身を undefined にするというのはわたしもやります。ところで const undefined ... だと型検査に失敗しませんか?

nobsun :

ああそうですねぇ.うまくいくこともあったと思いますが,一般にはだめですね.筆がすべった.:)

shiro (2007/07/30 22:53:29):

「使われ方によって型が決まるので」--- その使われ方を試行錯誤している段階の話なんですが、Haskellerは 最初に使われ方が頭に浮かんじゃうんでしょうか。 例えばどう書く.orgのトランプの問題で、 引いたカードの可能性をカードの数のタプルだけで [(Int,Int)] と表現すべきか、 積と和もついでに計算しといて [(Int,Int,Int,Int)] でやった方が簡潔になるか… どれが良いかはある程度上位の関数まで書いてみないとわからないじゃないですか。 [(Int,Int,Int,Int)] で書いてて、途中で [((Int,Int),Int,Int)] にしたら 余分なlambdaを書かずに素直な関数合成で済むことに気づく、とか。 そういうのは無くて、最初からもう型は見えてるもんなのかなあ。

shiro :

ああ、「使う方から型を見る」ってことは、トップダウンで考えてるって かな。だからxやyで試行錯誤する前に、wがもう出来てて、そこの要請にあわせて xやyを書いてゆくと。私は具体的なデータが目に見えないとピンと来ないんで 先に下の方のユーティリティ関数を書いていろいろデータを流しながら 考えることが多いんだけど、nobsunは逆なのかなあ。

nobsun :

トップダウンで書くこともあるし,ボトムアップに書くこともあります.でも常に型のプログラムを先に書きます.tramp問題なら最初は

type CardPair = (Int,Int)

n :: Int
n = 13

solve :: [CardPair]
solve = b2

xys :: [CardPair]
xys = [(x,y) | x <- [1..n], y <- [x..n]]

a1 :: [CardPair]
b1 :: [CardPair]
a2 :: [CardPair]
b2 :: [CardPair]
a1 = undefined
b1 = undefined
a2 = undefined
b2 = undefined

てな感じではじめます.常に型検査がパスするように定義を追加します.Haskellプログラミングは「型をプログラミングする」=「仕様をプログラミング」するという感覚です.型検査をパスするようにすること自体もプログラミングなので全然面倒だとは思わないんですよね.

shiro (2007/07/31 01:22:24):

あーこりゃ見事にトップダウンですねぇ。 私は最初にmultiplesToとかsumsToなどのユーティリティ関数を作って、 その出力をいじりながら a1, b1…と作ってって、最後に全体をsolveでまとめました。