2013/03/02
ピアノレッスン84回目
夕方からの時間をリハーサルに取られてるのであまり進まなかった。
- Bach: Well-Tempered Clavier Book I No. 3 (C♯ major)
- バッハは寝不足では弾けない。
- Scriabin: Sonata No.4
- IIの主題再現部の冒頭まで。
Tag: Piano
2013/03/02
型論争
ふたたびの盛り上がりを見せてる静的vs動的型論争だけど、これを「適材適所でしょ」 というtrivialな話で片付けたくないのは、双方の主張に(あまり意識されない)前提があって、 その前提をはっきりさせることには意味があるんじゃないかなと思うから。
それぞれの文化には慣習みたいなものがあって、そういうのは その文化における弱点を自然に回避するように形成されてるんじゃないかと思う。 だから一方の陣営がいくつかの具体的体験を頭におきつつ一般化して「この手の問題で困るでしょ」 と話を振っても、もう一方の陣営にとってそういうスポットは「わざわざやろうとは 思わないし、やる必然性も無いし、慣習にしたがっていたらそもそも思いつきもしない」こと だったりするんで、ピンとこない。
静的型チェックがあれば型エラーを機械的にはじけるよ、と聞いた動的型プログラマが 「でも型エラーなんてtrivialなものでテストやコンベンションで簡単に防げるし、 それより型で捕まえられない類のエラーの方がずっと大きいでしょ」と言う時、 彼の頭の中には「『<X型のリスト>を取り<整数>を返す』関数と『<整数>と<文字列>を取り<整数>を返す関数』を引数に取って『<文字列のリスト>を取って【{<整数>を取り<X型>を返す関数}を取って{<整数>を取って<整数>を返す関数}を返す関数】のリストを返す関数』を返す関数」みたいなものを書く、なんて事態は想定されてないのだと思う。だってそんなの書こうと思わないし、書いたらハマることはわかってるから もっと分解してわかりやすいように書くでしょ。
でもそういう妙な型を考えるとすんごくすっきり書けるケースってのがあって、そういうのをSchemeで書いてると「整数を返す関数を返す関数」が要るところに「整数を返す関数を返す関数を返す関数」を使っちゃった、なんてことでハマって型のサポートが欲しくなる。(printfデバッグしてもあんまり役に立たない。どっちも関数としてしか表示されないから。関数をレイヤごとに別クラスのオブジェクトにくるんでやれば区別が付くけど、それっていちいち型宣言してるのと手間は同じになるし)。
動的型の強みである時定数の大きなケースというのも、静的型開発では「そもそも非平衡状態は速やかに収束させるべきものである」という運用でやってるから(参考: これとかこれ)、なんでわざわざ不安定な状態を長引かせたいのかわからん、てことになるんだと思う。
私は『Land of Lisp』の性格テストではCと答えるどっちも欲しがりなんだけど、 こういう出発点の違いがあるから、 そう都合良く双方の良いところ取りはできないだろう、とは思っている。
「動的言語にアノテーションを入れて少しずつ静的検証…」ってことを書いたら 「それってgradual typing」って反応があって、 確かにそれも一つの解として視野に入れてるんだけど、 それがあるからって高階な型をばりばり使えるようになるかというと多分そうはならなくて、 動的な文化の出発点というのはあまり変わらないだろうな、と思う。
Tag: Programming
2013/02/28
システムの非平衡状態
昨日のエントリで、システム中のあるユニットを変更した時に、それに依存する部分を全部一気に変更するのでなく、システムを動かしながら徐々に変更を伝搬してゆく、という運用について触れたんだけど、具体例を書かなかった。というのは現場で出てくる具体例って色々なしがらみに依存して話が難しくなってることが多くて、そこらへん説明するのも冗長だってのと、個別のケースについて何とかしようと思えばどんな道具を使っても「頑張れば何とかなる」ってことが言えちゃうんで。実際の言語の使用感ってのはその「頑張る」をどのくらいに感じるかってところに帰着するんだけど。
それと、ある言語でプログラムする時はその言語で考えるから、その言語で書きづらい事態というのをそもそも想定しないというか、無意識にそういうスポットを避けられるように設計する。なので「こういうのが書きづらいじゃないか」と言われると「確かに素直には書けないけどそもそもそういうのを書きたいと思わないし。どうしても必要なら無理すれば書けるからいいんでない?」って感想になっちゃうことが多い。
そんなわけで、「変更」とか「不平衡」とか聞いても人によってイメージするものが違うんで、話がややこしくなるってのはある。
まあそれでも敢えてひとつ、単純化した例を考えてみよう。
プロジェクトメンバーを管理する。メンバーはidと名前と所属セクションを持つ。
data Section = Research | Production | HR | Management deriving (Show) -- Member id name section data Member = Member Int String Section deriving (Show)
あらゆるシステムがこのデータ型を前提に組まれている。
ところがある時、「複数のセクションに所属するメンバー」というのがありえることがわかった。
つまりMember
の定義は本来こうあるべきだったのだ。
data Member = Member Int String [Section]
これが正しいモデルなので、最終的には全てのコードをこちらに収束させたい。
でもMember
を使ってるシステム全てを一斉にアップデートすることはできない。
一部の、どうしても複数セクションを扱いたいユニットだけには複数セクションを見せて、
それ以外のユニットについては、複数セクションに所属するメンバーは
「そのうちのひとつのセクションだけが見えてればいい」とする。
(一応優先順位があって、最優先のものが見えるようにする。)
さてどうする。
Haskellだとどうするんだろ。MemberEx
みたいな別定義にしといて、
既存のコードに対しては MemberEx -> Member
とか
Member -> MemberEx
なアダプタを噛まして対処するのかな。
慣習がよくわからない。
CLOS系なら変える必要があるのは一ヶ所だ。こういう元コードを:
(define-class <member> () (id :init-keyword :id) (name :init-keyword :name) (section :init-keyword :section) )
下のように変えておけば、メンバーが一つのセクションだけに所属すること前提のコードと
複数セクションを見るコードが混在した不平衡状態のまま運用を続けることができる。
change-class
メソッドも書いとけば、
動作中のコードにロードして動かしつづけることもできる。
(define-class <member> () (id :init-keyword :id) (name :init-keyword :name) (sections :init-keyword :sections :init-value '()) ;; for the backward compatibility - remove after migration is completed (section :allocation :virtual :slot-ref (^o (car (~ o 'sections))) :slot-set! (^[o v] (set! (~ o 'sections) (cons v (delete v (~ o 'sections)))))) )
まあ静的型でも「フィールド直接アクセス禁止して常にアクセサ経由にしとけばいいじゃん」とは言える。その意味では程度問題。当初からどのくらいこういった事態を見越して書いとくかって話になる。
それに、「別定義にしてアダプタを噛ませとけば、後から直してゆく時に直すべき箇所が見つけやすいだろう。やっぱり静的型がいい」って考えもあるだろう。
あるいは「sectionとsectionsの関係がill-definedで、こんないいかげんなシステムは既に崩壊してる。このシステムの存在自体がバグである」という立場もありだろう。(自分は「え〜これとそれはone to manyって最初に念押ししといたじゃないですか〜なんでいつの間にかmany to many前提になってるんです〜?」みたいな話がありすぎる環境にいたから、バイアスがかかってると思う。)
結局何がメリットかってのはどの立場を取るかで変わってくるから、「××がメリットなんだ!」という記事は「俺はこういうケースで楽をしたいんだ!」という主張と読むべきなのかもしれない。
ちなみに上のように変えた後で、「いや、やっぱsectionはひとつで良かったわ。戻しといて。」ってなることも、よくある。
Tag: Programming
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
2013/02/23
GaucheでもLand of Lisp
『Land of Lisp』、皆さんのお手元に届き始めたようだけれど、 ぜひ読むだけじゃなく例を打ち込んでゲームで遊んでみて欲しい。
けれどもそのためにCLISPを入れるのも面倒だなあ、 という人のために、Land of Lispのゲームを Gaucheに移植しておいた。
CLISPを入れちゃった人でも、「このCLのイディオムはGaucheではどう書けるんだろう」 と思った時のサンプルにしてもられば。
★ ★ ★
(追記2013/02/25 09:15:08 UTC): 第10章で作る進化シミュレーションで、長く走らせてると
種の分化が起きるって話が出てくるけど、実際にgenesに違いが出てるのかどうかを
ダンプして調べるのは少々煩わしい。そこで遺伝子の傾向の違いを色で表示する
バージョンを作ってみた。リポジトリのevolution-color.scm
参照。
これは30万日後の様子。
Tags: Programming, Lisp, Gauche
Comments (0)