Island Life

2013/03/15

静的チェックで困った話

Gaucheは型付けも含めて色々動的な言語だけれど、コンパイル時にわかることは なるべく検査するようにしている。例えばローカル定義された関数は、その定義が 変更されないかどうかわかるので、アリティチェックの対象となる。

(define (start/end-vector->shape Vb Ve)
  (define (interleave a b)
    (cond [(null? a) b]
          [(null? b) a]
          [else (cons (car a) (interleave b (cdr a)))]))
  (apply shape (interleave (s32vector->list Vb) (s32vector->list Ve))))

これは今適当に引っ張ってきた関数だけど、interleaveはローカル定義されて 変更されていないので、常に2引数で呼ばれることがわかる。コード中で、interleaveが 1引数や3引数で呼ばれている箇所があれば、コンパイル時にエラーにできる。

グローバルに定義されている関数の場合はちと面倒だ。 実行時に関数定義が変更される可能性があるので、 コンパイル時に呼出側を静的に拒否することはできない。 ただまあ、実行時の関数の置き換えをやりたいのは開発中とか緊急時で、 普段はそうそう必要なものでもないので、Gaucheではユニットテスト時に モジュール毎に検査するのことを推奨している(test-module)。

例外はdefine-inlineで定義された関数で、インライン展開は コンパイル時に行われるから、この関数は開発者が「これは動的に置き換えない」と 宣言したものと解釈でき、コンパイル時の静的検査の対象にできる。 インライン展開される標準関数(consとか)が、シャドウされない限り コンパイル時チェックされるのもこの理由。

さて、ここまでが前置き。

以前、map系関数のインライン展開を試していた時に、 コンパイル時アリティ検査が干渉して困ったことがあった。 実際はmapじゃなかったんだけど、話を単純にするためにこんなナイーブなmapの定義を考える。

(define-inline (map proc xs . xss)
  (if (null? xss)
    ;; (map proc xs) で呼ばれた場合
    (if (null? xs)
      '()
      (cons (proc (car xs)) (map proc (cdr xs))))
    ;; (map proc xs ys zs ...) で呼ばれた場合
    ... 定義省略 ...))

そんで、このmapにローカルで定義された関数を渡したとする。

(define (foo as bs)
  (define (bar a b)
    ...)
  (map bar as bs))

mapがインライン展開されるとこうなる。

(define (foo as bs)
  (define (bar a b)
    ...)
  (let ([xs as]
        [xss (list bs)])
    (if (null? xss)
      (if (null? xs)
        '()
        (cons (bar (car xs)) (map bar (cdr xs)))) ; !!
      ... 複数引数の処理 ...)))

!! の行で、barが1引数で呼ばれている。コンパイラがこれを弾いてしまったのだ。

実際には !! の行は実行されることがない。 ローカル束縛をちゃんと追跡すれば、(null? xss) が常に#fであることが コンパイル時にわかるので、1引数の場合の処理部分はコンパイル時に削除されるべきで、 そうすれば問題は生じなかっただろう。

けれどもこの分岐が単純な (null? xss) でなかったらどうだろうか。 例えば (weird-map info proc . args) みたいなインタフェースで、 procのアリティが外部関数の呼び出しで計算されるものだとしたら?

(define-inline (weird-map info proc args)
  (if (= (calculate-arity-from-info info) 1)
    ;; procは1引数
    ...
    ;; procは複数引数
    ...))

こうなってくるとコンパイル時に静的にprocのアリティを検査して弾くことは難しくなる。

今回はアリティだったけれど、同じことは実行時に属性が検査される あらゆる場所で起こり得る。例えばこんなコード:

(define-inline (do-somethin x)
  (cond
    [(check-it x) (string-length x)] ;; check-itが#tならxはstring
    ...))

プログラマはcheck-it#tを返したらxが文字列であることを 知っているが、コンパイラにはわからないとする。この関数が 次のようなコンテキストでインライン展開された場合:

(let ((n 3))
  ...
  (do-somethin n)
  ...)

整数がstring-lengthに渡される、という呼び出しがコード中に 出現することになる。そこは一見矛盾しているけれど、 実際には決して実行されないのでコードのコンパイルは通ってくれないと困る。

ナイーブな解法は、こういう箇所にその都度注釈を入れてコンパイラに 情報を伝えてやることだろう。Common Lispの記法を借りればこんな感じ:

  (cond
    [(check-it x)
     (locally (declare (string x))
       (string-length x))]
    ...))

だけどこういう注釈をちまちまつけてると、コンパイラのご機嫌を取ってる気分に なってくるのも確か。さらにdo-somethinの定義が自分の触れない場所に あったりすると不満が募る。(それで不満に感じないなら最初から静的型付き言語を使ってるって)。

問題の根源はコンパイラが十分に情報を持っていないことにある。 静的型付き言語は「コンパイラに型という言語でもって情報を伝える」っていう 方針なわけだけど、型という言語体系にうまく載せられる情報と載せにくい情報があるからなあ。

ひとつの手は全てのプログラムをコンパイラが見ることだ。Stalinがやってるけど、 標準ライブラリも含めてコンパイル時に全部を見ることで、今回の場合だとcheck-it#tならxが文字列であることを演繹できる。でもこの方法はプログラムが大きくなると コンパイルに時間がかかりすぎて破綻する。

むしろ、コンパイラが使える材料を一種の知識ベースのように 考えて、後付けでいろいろな情報を放り込んでやれないかな。 ここの例でいえば、「(check-it x)が#tを返したらxはstringだよ」っていう情報を check-itの実装者以外でも後から注記できるようにするとか。

ライブラリ関数が使われる箇所でコンパイラが実際にどういう情報を必要とするかを、 ライブラリ実装者があらかじめ全て見通しておくことはできないと思う (「それができるように型システムを整備する」というのが静的型の発想だけど、 そこで完全を目指すと「仕様を全部形式言語で書いておく」なんてところに 行き着いちゃうんじゃないだろうか)。

Lispのカルチャーの一つは、「システムを書いた人より、それを使う人の方が賢い」 という前提だ。なぜなら人は道具を使うことで、その道具に関する新たな知見を「発見」して ゆくから。後付けで作者以外が知識をばらばらに追加してゆくのはこういう思考に 合うかもしれない。

Tag: Programming

2013/03/14

ピアノレッスン86回目

  • Bach: Well-Tempered Clavier Book I No. 3 (C♯ major)
    • Prelude, Fugueともに曲としてまとまってきた。細かいところを磨くのにもう一週。
  • Scriabin: Sonata No.4
    • 超スローだけど最後までなんとか通し。何度も弾いているとだんだん曲の組み立てが見えてきた。ロマンチックなようだけれど構造としてはかなり厳格で緻密に作られている曲だなあ。

Tag: Piano

2013/03/13

リベラルアーツ

東工大と比較して、MITでのリベラルアーツ教育が充実しているって記事なんだけど、 なんか論点というか見るべきところが微妙にずれてるような妙な感じがした。

私が大学生だった20年前と現在で日本の大学も変わっている、とか、東大は総合大学だから事情が違ったとかいう可能性はあるけど。

MITがリベラルアーツに力を入れる理由として、MITの文科系学部の先生の言葉を引いている。

大学で電子工学や機械工学を学び、そこで得た知識でコンピュータや自動車を作るとしても、その製品は社会に存在し、人間に使われるわけです。ならば、作り手に回る理系の人間こそ、社会と人間のことを徹底的に知っていなければ、理解しなければ、コミュニケーションがとれなければダメです。だから『教養』を身につける必要があるのです。

これと全く同じことを、大学に入った時にさんざん聞かされた。 だから別に日本の大学がこういうことを考えていないってわけじゃないんじゃないかなあ。わざわざ「あのMITではこう言っている!」って引っ張ってくる言葉ではないような。

とはいえ東大の教養課程がこの記事にあるMITのように充実してたかというとそうでもないんだけど、それは理念の問題じゃなくて実践の問題だったんじゃないか、と思う。まあ、それは実際に記事中で触れられているんだけど:

かつて、日本の大学では今よりも教養教育を重視していたけれど、さきほど上田さんがおっしゃった「学び方――how to learn」については、ちゃんと教えてこなかった。

ところが記事の後半から、(最初の記事の「日本の大学では実学重視の要請を受けて教養を削ってきた」という流れを受けて) いやMITでもライティングやプレゼンなど実学指向のトレーニングもしているよ、という話題になる。

ただ知識を習得するだけでなく、論評し、考えをプレゼンする技法も身につけるわけですものね。教養の授業でありながら、実学も織り込まれている。教養と実学が二項対立ではなく、むしろセットになっている。

これを「教養と実学という二つのものをセットにしている」ってとらえるところからボタンを掛け違えているような気がする。

何にせよ学ぶということは表現することと表裏一体だ。頭の中に入れただけで「分かった」と「分かってるつもり」の区別をつけるのは非常に難しい。脳は自分に見えるものしか見ないので、「分かっていないこと」を見ることができないからだ。欠けている部分を自覚するには、一旦外に出して、他者の目を通してフィードバックを受ける必要がある。

そして、「自分が何を分かっていないかを分かる」ことはおそらく最も重要な「学ぶ技法」だ。

ライティングやプレゼンというのは実用になるテクニックという以前の、学ぶための基本的テクニック、あるいは学びの基礎練習だ。別々のものをセットにしてるんじゃなくて、必要なことをやってるだけ。

だから問題の根っこは教養の軽視とかいう以前にあるような気がしてならないし、この記事を見て「じゃあとにかく教養科目を充実させよう!」とか「実学指向のプレゼンテクニックも教えよう!」って流れになるとすれば、なんか違う。

何を教えるにしても、まずきちんと学ぶ場を作る。それがないとその上に何を作ってもすぐ崩れちゃうんじゃないかなあ。

(余談。日本で演劇教育がほとんど根付いていないのは何でかってずっと疑問に思っているのだけど、表現することに力が入れられてないっていう要因はあるのかも。教養の典型例としてシェークスピアが引き合いに出されることがあるけど、あれは読んだだけじゃわからないよ。演って初めて「わかる」。)

Tags: 教育, 大学

2013/03/11

時定数の大きな話とCL

こないだのコードの変更と型付けにちょっと関連して、向井さんが 変更の時定数が大きい実例を書かれていた。 GUIでの座標管理。これまでひとつの座標系で済ませていたところに、 複数の座標系を導入しなければならなくなった、という話。

古くて新しいGUI座標系の型の話

common-lispではいい手はあるのかな? generic functionやオプショナルな型チェックはできるだろうし、マクロを使って型チェックを導入する(しかもリリースビルドではチェックを省略するとか)できるだろう。が、そう簡単にはいかないのではないか?という気がする。どこにチェックするかを指定する、ということは、テストを書くのと同じぐらい大変だ。型レベルの変更は、コンパイラが網羅的に検証してくれるので、楽だ、というのがここでの要旨なので。

Common Lispでやるとすれば、「これまで単なる整数で扱っていたものをオプショナルに実行時型付きで扱えるようにして、クリティカルな部分から徐々に変えてゆく」みたいな手順になるかなあ。

例えばこんなマクロを書いといて:

(def-coords-macro global)
(def-coords-macro root)
(def-coords-macro display)

とする。そんで、例えばルートウィンドウの座標系を受け取る関数があったら:

(defun root-window-hoge (rx ry args)
  ... 
  (let ((gx (convert-to-global rx))
        (gy (convert-to-global ry)))
    (global-window-hoge gx gy ...)))

checking-root-coordsマクロで定義をラップする、などしてゆく。 あと座標を他の関数に渡すところをboxで包んでゆく。

(checking-root-coords (rx ry)
  (defun root-window-hoge (rx ry args)
    ...
    (let ((gx (convert-to-global rx))
          (gy (convert-to-global ry)))
      (global-window-hoge (box-global gx) (box-global gy) ...)))
  )

この段階では、checking-root-coordsマクロでラップされた関数は、 タグ付けされた座標 ((root . 100) とか) も生の数値も受け入れるので 部分的に変更していっても全体は崩さない。呼び出し側で間違えた型をつけたときだけ エラーが出る。

そんで、変更が全体に普及したと思ったら、def-coords-macro の定義を変えて、 unbox が生の数値を受けとるとエラーを出すようにする。

まあ、これもそこらじゅうにキャストをつけて回るようなものなんで、 静的型でもってそれぞれの座標を別の型にして修正して回るのと手間は変わらんのじゃないか、 と言われればその通りではある。 C++なら暗黙の型変換使えば、部分的に修正中でもコンパイルは通せるだろうし。

ただまあCLだと、型宣言とか性能計測用マクロを仕込むためだとかで こんな感じの全体を囲むマクロを作ることがちょくちょくあるので、 そういうのが既にあるところに乗っかる形でアドホックな実行時型チェックを 楽に仕込める、こともある。

もっとも現実には、こういうアドホックなマクロは締切りに迫られて 大急ぎで書かれるせいでドキュメントとかが全然無くて、 後からプロジェクトにjoinした人が暗号のようなマクロを解読するハメになって マクロ嫌いになったりするのだが。

Tags: Programming, Lisp

2013/03/08

ピアノレッスン85回目

  • Scriabin: Sonata No.4
    • IIの主題再現部途中まで。展開部からは超スローで。なかなかコーダの第一主題大爆発のところに到達しない。
    • 「これまでやった曲の中で一番難しいんじゃない?」そうかも。多声的で和声的でリズムも複雑。楽しい。

Tag: Piano

More entries ...