2013/02/27
型付けと変更の時定数
静的型付けvs動的型付けの永遠の議論で良く出てくる論点に、 「変更に強いのはどちらか」という話がある。
- 動的型付け陣営は、「後付けでシグネチャを変更したくなった時に、 既存のモジュールに手を入れずに拡張できるから動的型の方が変更に強い」と言う。
- 静的型付け陣営は、「静的型付けだと、型を変更したら合わせて変更すべき箇所を コンパイラが漏らさず見つけてくれるので、静的型の方が変更に強い」と言う。
理由が反対を向いてるのに結論が同じになるのがおもしろいが、 これは、同じものについて議論してるんだけど、 見てる場所が違うせいだと思う。 双方の立場の一番の違いは、動的型付け言語がシステムの非平衡状態を重視するのに対し、 静的型付け言語は平衡状態を重視していることなんじゃなかろうか。
ある安定した系がある。その一部に変更が加わる。するとその影響が徐々に波及してゆき、 十分な時間が経つと(元の状態とは違うけれど)再び安定した平衡状態に落ち着く。
静的型付けは、平衡状態で明らかな矛盾が起きないことを保証する強力な道具だ。 変更が加わったら、それによって影響を受ける場所を速やかに見つけて、 なるべく短時間で再び平衡状態に持ってゆくべきだ、という前提がある。
動的型付けが変更に強い、と言っている人は逆に、 「変更途中だけど動かさないとならない」とか「動いているものを止めずに変更してゆく」 といった非平衡状態を頭に置いている。 理論上は最終的に平衡状態に収束するはずだけれど、そこまでの時定数が 現実に見えるほど大きいことを前提にしている。 そして、変更は次から次へとやってくるものだから、 システムは常に、平衡へと向かう動的な状態に置かれる。
ソースコードを全て個人あるいは1チームがコントロールできる環境なら、 システムの時定数は小さい。一ヶ所APIを変更したら、影響を受けるモジュールも 一緒に変更して同時にコミットすればいい。そういう場面では静的型言語の方が圧倒的に有利だ。
逆に時定数が大きくなるのはどんな場合だろうか。 自分のチームがあるライブラリXを作っていて、さらにXを使ったアプリケーションAを作っているんだけど、 他のチームがやはりXに依存するアプリケーションB, C,...を作っている、なんて場合がそうかもしれない。 Xは比較的新しい技術で、どういうAPIが正しいのかまだはっきりわかっていない。 現在のAPIは、今分かっている知見に基づく暫定版だ。 B, C,... のアプリケーションの中には、既に実運用に投入されているものもあれば、 まだ実験段階のものもある。
さて、自分のチームでAを作っているうちに、どうも今のXのAPI
SolveX :: Foo -> Bar
は解くべき問題を正しく抽象化していない、
ということがわかった。直すには、パラメータや返り値を増やすとか、
データ型Foo
やBar
の設計をやり直す必要がありそうだ。
問題なのは、じゃあ SolveX
を変更して影響を受けるところを
いっせーのせで変えましょう、というわけにはいかないことだ。
- 既に実運用に入っているアプリケーションの変更には、工数がかかる。 単に型を合せればokではなく、その変更が既存のユースケースにちゃんと合うかどうか 検証しないとならない。今その工数を取っている余裕がない。
- 一方、抽象化の設計が正しいかどうかというのは、実際の問題に適用して 使ってみなければわからないものだ。だから「変更してもいいよ」という 実験的なプロジェクトや自分のチームで作ってるプロジェクトのために、 新たなAPIをさっさと実装して使えるようにする必要がある。
これが時定数の大きな非平衡状態だ。 そこでまず新たなパラメータを導入したAPIをライブラリの開発版に足して、 それをアプリケーションAで使ってみる。 いけそうだったら外のチームに伝えて、実験段階のものから暫時新しいAPIに移行してもらう。 その変更が徐々に伝わって、最終的に全部が新たなAPIに移行するには 年単位の時間がかかるかもしれない。
醜いけど SolveX2
という新たなAPIを作って使う、という手がある
(醜い、というのは単に名前が増えるいうだけではなく、新しい知見では
新APIの方がより完全にSolveXを体現しているので、
本来なら新APIにSolveX
という名前を与えたいからだ)。
あるいは変更がまばらなら、ライブラリのバージョニングで対応するという手もある。
けれどこういう変更がひっきりなしに起きるとしたら? バージョニングは簡単に破綻する。一次元のバージョンをつけられないからだ。 ライブラリが解決するサブ問題SolveXaとSolveXbとSolveXcについてそれぞれこういう変更が起きて、 色々な開発ラインが各APIの特有の組み合わせに依存して、 しかもバグフィクスはAPIとは関係なしにロールアウトしないとならない、とか。
Common Lispの開発ではこの手の「以前のバージョンを壊さずに後付けで 新しいAPIも使えるようにする。徐々に新APIに移行して、全部移行し終わったら 以前のバージョンのサポートを落とす」っていうのをかなり頻繁にやる。
静的型付け言語だと…型によるオーバロードあたりで何とかするのかな。 ただ。オーバロードは人間が覚える名前を少なくする効果はあるけど、 実行時に名前から関数引っ張ってきて呼び出すなんて操作をしてると 別名をつけてるのとあんまり変わらないんだよね。
それにカリー化と高階関数と型推論を多用する静的型付け言語だと、オプショナルな引数や キーワード引数で変更に対応するという手法と相性が悪い (できなくはないみたいだけど、 無理してる感じがする。)
で、結局どっちが良いかって話だけど、私としてはどちらのメリットも捨てがたいんで、 開発形態によって選べるといいなと思うんだけどね。
一つのシステムであっても、初期のいろいろなものが流動的で変更の時定数が大きい状態が、 だんだん枯れてきて平衡状態に落ち着くなんて経緯をたどるものがあるんで、 できれば言語そのものは変えないで両方のいいとこ取りをしたいんだけど、 でも例えばHaskell書くときとGauche書くときでは発想段階から違う気がするから、 都合良く両方を融合するってことは出来ない気がする。
動的陣営としては、原則動的で、徐々にアノテーションを入れて必要な部分だけ 静的検証が別途できるようにしてゆく、って方向が現実的かなあ。
(追記2013/03/01 03:03:22 UTC): 「大規模開発なら動的なのか」って感想を見たけど、規模よりはコードのコントロールの問題だと思う。大規模でも関係する全部のコードを一気に修正できるなら静的がいいし、小規模でも自分の制御の及ばないところでいろんなふうに使われちゃってたら一気に修正するわけにはいかないだろう。
Tag: Programming
keigoi (2013/02/28 22:33:12):
shiro (2013/03/01 01:17:13):
山本和彦 (2013/03/01 02:06:02):
shiro (2013/03/01 02:41:57):
山本和彦 (2013/03/01 03:04:28):
shiro (2013/03/01 03:22:25):
山本和彦 (2013/03/01 03:22:54):
山本和彦 (2013/03/01 03:33:27):
garriguejej (2013/03/01 06:02:11):
garriguejej (2013/03/01 06:14:37):
shiro (2013/03/01 09:00:22):
shiro (2013/03/01 09:05:34):
山本和彦 (2013/03/01 10:02:44):