Island Life

< ソーシャルな評価と誤魔化しの防止 | 演者のエナジー (ピアノレッスン10回目) >

2011/07/19

動的型のメリットは「決断の遅延」かもしれない

Togetter - 「動的型言語のふわふわ感」 読んでて思ったんだけど。

静的型付けと動的型付けで何が一番違うかって考えてくと、「実行前に済んでること」と「実行時」とをどのくらい分けるか、というところに帰着するかもしれない、と思った。

静的型付けは実行しないでも型エラーをコンパイラが見つけてくれる、というのは、つまり実行する前にプログラムが出来上がっているってことを前提にしているわけだな。程度問題ではあるけれど、実行前のどこかの時点で線を引いて、その時点で分かっている情報の整合性を型という枠組みで保証しましょ、ってわけだから。

「実行する前にプログラムが出来上がっている」なんて当然じゃないか、と思うかもしれないけれど、本当にそうだろうか。

上のtogetterで、「Rubyでは 1 + "hoge" が実行時エラーになるけれど、それは型エラーなんだから実行前に捕まえられた方がいいでしょ」という議論があった。でも、それは型エラーじゃないかもしれない。単に、そういうケースへの対応を実行前にはまだ決めていなかっただけかもしれない。

cl-user(1): (defmethod plus ((x number) (y number)) (+ x y))
#<standard-method plus (number number)>
cl-user(2): (plus 2 3)
5
cl-user(3): (plus 1 "hoge")
Error: No methods applicable for generic function
       #<standard-generic-function plus> with args (1 "hoge") of
       classes (fixnum string)
  [condition type: program-error]

Restart actions (select using :continue):
 0: Try calling it again
 1: Return to Top Level (an "abort" restart).
 2: Abort entirely from this (lisp) process.
[1c] cl-user(4): (defmethod plus ((x number) (y string)) (format nil "~a~a" x y))
#<standard-method plus (number string)>
[1c] cl-user(5): :continue 0
"1hoge"

(Lispに詳しくない人向けの説明: (plus 1 "hoge") というメソッドは未定義なので実行時エラーで止まったけど、そのエラープロンプトから適切なメソッドを追加して実行を再開したので、ちゃんと呼び出し元に答えが返ってくる。)

もちろんこれは極端な例だ。何でもかんでも、実行エラーが出たらその場で直せばいいじゃん、というわけにはいかない。とりわけ、プログラムの作成者とユーザが別々であることが普通になった現代では。

けれども、「プログラムは実行前に完成しているべきである」という前提を疑ってみるのは無益ではないと思う。

例えばユーザによりカスタマイズ可能なプログラム。ユーザが入手した時点でプログラムは完結しておらず、ユーザの手によって使われながら(そのユーザにとっての)完成形へと徐々に進化してゆく、とみなすこともできる。もちろん、プログラム本体とユーザによるカスタマイズ部分を完全に分けて、前者はコンパイル時にがちがちに検査をかけ、後者を実行時に解釈する、という実装は可能だ。でもその分離は本質的なものだろうか? 単に「コンパイル時と実行時の間に線を引かなければならない」という実装上の都合でそうなっているだけかもしれない。

止まること無く走りつづけて、その時その時の状況に合わせてふるまいを変えてゆく必要があるプログラム、というのもある。これも極端な例としては実行中のサーバにランタイムパッチを当てるなんていうケースがあるが、そこまでアドホックな話でないにせよ、あらかじめ変更を見越して後から開発されてくるモジュールを柔軟に読んだり削除したりするようなアーキテクチャというのは考えられる。オンラインゲームで機能を追加してゆくとか ←実際の現場ではさすがに本番系のモジュールを実行中に入れ替えなんてやってないだろうけれど、それも単に今のプログラムというのがそのような実行時の動的変化を安定して扱えないだけだから、かもしれない。ゲームで言えば、プレイしながら振る舞いを調整したいってのもあるね。これも今は振る舞い部分だけ動的スクリプトにしたりするけど、それも本質的な分離ではなく、実装上の都合かもしれない。

思考の道具として、あるいは実行可能なアイディアとしてプログラムを見ることもできる。黒板にメモして、あちこち書き換えながらアイディアを発展させてゆくように、プログラムをあちこち書き換えながらアイディアを詰めてゆく。黒板との違いは、そのプログラムは動かせるということだ。そして、動かしている最中に新しいアイディアを得て、その場で書き換えて(最初から再実行するのではなく)実行を続行したい、というケースは十分考えられる。シミュレーションのように抱える状態が大きいものであれば特にそうだろう。もちろん、ポータブルな形で状態をダンプできるようにしといて、停止→ダンプ→書き換え、再コンパイル→リストア→続行、というワークフローをとることはできる。が、それはやっぱり実装の都合だ。

これらの例をもって、だから静的型付けはだめだ、と主張するつもりはない。現代のプログラムの圧倒的多数は、実行前にプログラムが閉じて完成していることを要請されているだろうし。上に挙げた例でもいちいち注記したように、アーキテクチャを工夫することでフェーズを分離し、静的型付け言語で実装することは可能だ。もちろん、可能だからといってそうすべきであるということにはならない。

動的型付けを好むプログラマが一定数いるのは、プログラマが触っている「開発途中のプログラム」というのがまさに「完成前に実行したい」ものだから、なのかもしれない。とりわけ、「何を、どう作るべきか」がまだ見えない状態でとりあえずデータをいじりながら発想を得たい、なんて時には。

Haskellerと話しているとまず型から考えるみたいで、ある意味「こうあるべき/こうあって欲しい」というイデアから演繹的にコードを導き出してるようにも見える。勝手な思い込みだけど。一方、手元にぐにゃぐにゃした不定形のデータがあってそいつをこね回したいって時には、最初に手をつける時に構造や意味を決めすぎないようにしないと話が進まない。動的型付けは、「この操作はこういう意味を持つ」と定義する決断を先延ばしにしていると言えるかもしれない。先に構造と意味を決めてからコードを書くのではなく、動くコードを書いてからその意味を考えて構造をいじる。こう書くとエンジニアリングとしてはかなり危なっかしい感じがするけれど、一般的に「なにかをつくる過程」だと思えばそれほど不自然な話ではない。

とはいえ、Lispプログラムだって実行前にわかってることはたくさんあるんで、わかってる範囲ではコンパイラに働いて欲しいんだな。動かしてこねこねしているうちに形が定まってきたら、そこでアノテーションを入れて静的検査を強化できたらなあと思うことはよくある。

Tags: Programming, Lisp, Haskell

Past comment(s)

Rui (2011/07/19 20:27:55):

静的型付け言語でも、コンパイル時の型エラーを実行時にエラーを投げるコードに置き換えてコンパイルを通してしまうコンパイラがあれば、完成してないコードでも実行できると思うんですよね。

mattn (2011/07/20 01:15:16):

動的型付け言語はランタイムで挙動を変えられるのもウリだったりしますね。

https://gist.github.com/1094128 (ただしlvalue-operatorですが...)

osiire (2011/07/20 03:25:20):

「何を、どう作るべきか」がまだ見えない状態でとりあえずデータをいじりながら発想を得たい、というスタイルは便利だと思います。やってみないと分からないこと多いですし。 そこで私は、静的型付け言語でも、あとから考えようと思ってるところはassert falseにしてコンパイル通して動かすという手を使っています(OCaml)。Haskellでもundefinedというのがあるらしいです。

nobsun (2011/07/20 03:39:12):

Haskellで書くとき型から考えてはいきますが、型の内容を固定してから考えているわけではないです。なんだかわからないものをいじりまわしてはいるものの、いじりまわしているものに名前を付けて同定したいと思うのです。さっきのと、こんどの、よくわからにものは、同じもの、それとも、違うもの?ということを考えたいという感覚かなと思います。

shiro (2011/07/20 04:45:12):

> Ruiさん、oshiireさん: 私もHaskell書くとき(toy programくらいしか書いたことありませんが)に、とりあえずコンパイル通したい場合はundefinedにしときます。ただ、これから書くもののplaceholderとしては良いんですが、既に書いてしまったものがあって、でも部分的に扱う型を拡張したい、なんて時にどうすれば良いのかよくわかりません (コメントアウトしてundefined、というのは結構面倒なので)。しかもその「既に書いてしまったコード」が別の経路で呼ばれる可能性があったりするとなお厄介。Lispの場合は、caller siteを変えずにcallee側を互換性を保って拡張するのが簡単なのですが…

> mattnさん: まあ、あんまり無制限に変えられるようにするとこんどは最適化の頭痛の種になるんで、言語設計の立場からはバランスが難しいですけどね。でもランタイムの挙動を見て動的にコンパイルするようなテクニックが普通になってきたから、バランスポイントも変わってゆくかも。

> nobsun: いじり回しているものの性質を分かる範囲で同定したいっていう感覚はわかるんですが、型というのは余分なコミットメントが必要な気がしちゃうんですねー。ある意味、契約の表明じゃないですか。ライブラリやユーティリティ関数を書いている時に顕著なんですが、単純化かつcontrivedな例として、リスト中から要素を探してごにょごにょするって操作が必要そうだなあ、ちょっと書いてみるか、なんて思ったとします。要素の有無だけを知りたいのか、見つかったところから先に対して何かしたいのか、探す操作が必要なことはわかってるんだけど、それがどう使われるかはまだはっきりしない。そんな時とりあえずLispで言うmemberを書いといて、述語としても使えるし、戻り値を部分リストと見ることもできる。他にも思わぬ使いようがあるかもしれない。そんな曖昧なユーティリティをボトムアップに作って組み合わせてゆくうちに、だんだん欲しいAPIが見えてくる、なんてことがあります。これが、ちゃんと型付けできるように書くとなると、述語を返すのか、部分リストを返すのか、あるいはMaybeモナドにして要素の有無を部分リストの両方の情報を返すのか、使う前に決めないと書けない じゃないですか。その判断を先延ばししたいんですよねー (memberの例はtrivialですが、もうちょい複雑なデータ構造では、こういうことはよくあります。)

nobsun (2011/07/20 13:45:25):

う〜ん。同定すること自体「余分なコミットメント」ということなのかなぁ。「memberを書いといて..としても使えるし、..としても見ることができる。」というのは十分前もって決めているような気がしますが。。。

shiro (2011/07/20 16:42:34):

私の感覚では決めるよりは先延ばし感があります。「…としても使えるし…」みたいのはわりと後付けの理由で、「探す過程でこの情報も取れたけど捨てるのもったいないから返しとくか。述語としても使えるし。」ってノリで、堂々と「述語として以外にこういう情報も返してますよ」とコミットする感じではないとうか。「何となく使うかもしれないから余分なデータをオプショナルでぶら下げておこう」とか「何となく使うかもしれないからオプショナル引数取るようにしておこう」とか。「AとBから関数Xを共用してたんだけど、Aから呼ぶ際に余分な引数を渡したくなった。それがXの動作について本質的なものなのか、それともAの特殊事情なのか、まだ良く分からない。前者ならXの必須引数を変更してBも変更するし、後者ならAが呼ぶためのX'を別に作るけど、まだわからない段階ではXにオプショナル引数足してBは変えずにとりあえず共用しとこう」とか。

kinaba (2011/07/20 22:45:20):

> 静的型付け言語でも、コンパイル時の型エラーを実行時にエラーを投げるコードに置き換えてコンパイル EclipseのJavaコンパイラとかそうですよね。自分はこれはとても便利に使っていますが、実際どのくらい利用されているのかはわかりません。

Bak. (2016/11/14 04:10:04):

後半より冒頭の方が同意できました; 型は高速化のための最適化に思えて、型を強制する言語が「早すぎる最適化を強制する」、って感覚があります。なので本当は、型無しで書いて完成か、スプリントの中で型を足して高速化して完成か、目的によって選べる言語があると良いだろうなと思っています。

Post a comment

Name: