TL/1 の呼出規約

私は以前にプログラミング言語 TL/1 のトランスレータを作った。 その他、 TL/1 の面白いと思った部分を取り上げて何度か記事にしている。 私が TL/1 を紹介するにあたって根拠にしているのは月刊 ASCII の 1981 年 1 月号であり、その号には TL/1 のサンプルとして連珠のプログラムが載っていて、自作のトランスレータでその連珠を変換できることも確認している。

TL/1 トランスレータ - 主題のない日記

しかし、確認するにあたって、掲載されていた連珠のプログラムに明らかな間違いがあることも発見している。 仮引数が二個の関数を実引数なしで呼び出している個所が存在するのだ。

状況を単純化した例としてこのようなものを考えよう。

FUNC SQUARE

BEGIN
  WRITE(0: SQUARE)
END

SQUARE(X)
BEGIN
  RETURN X*X
END

関数 SQUARE は引数をひとつ取る関数であるにもかかわらず呼出側では実引数を与えていない。 TL/1 では最初に関数を宣言する段階では引数の情報を持たないので、パーサは SQUARE を呼び出している個所をエラーにすることは出来ない。 コンパイラがこの間違いを検出するとしたら 2 パスにする必要があるが、古い時代の限られたリソースでコンパイルするとしたら厳密な処理はしなかった (エラーとして検出しなかった) 可能性は充分にある。

私は TL/1 については資料を読んだだけで実際に動く環境を持っていなかったのだが、実際に連珠のプログラムを当時のコンパイラコンパイルしてみたという方から情報をもらった。 実引数の数が間違っている連珠のプログラムは当時のコンパイラコンパイルできたそうだ。 しかも、ゲームとして問題なく動作しているように見えるとのことだ。 このことから、 TL/1 の 1981 年当時のコンパイラでは引数の後始末を呼出側でやっているものと推測される。 スタックがずれてしまえば動作どころではなく暴走してしまうのだから、スタックの整合性は保たれていると考えられるからだ。 呼出規約が stdcall や PASCAL のような方式であれば、実引数の個数が間違っていても変数の中身が一時的におかしな値に見えるだけでスタックのずれは起こらない。

Document ID: 6640b10004b68b64362e7c7723299e0b

強い非保証

前回はプログラミング言語の挙動は何によって保証されるかということを取り上げた。 保証していないが現時点で実際そう実装されているということがあてにされてしまう場合が頻出すると追認する形で仕様になってしまう場合も少なくないのだが、保証しないという状態を更に強調した事例を思い出した。

プログラミング言語 Gomap (連想配列) である。 Go の mapイテレーションの順序は最初から保証はされていなかったが、データの数が小さい場合には一定の順序になっていて、それに依存するコードが結構あったらしいのである。 そこで Go は乱数を使ってまで毎回順序を出鱈目にするように変更された。

Go 1.3 Release Notes - The Go Programming Language

保証しないという強い意思を感じる。 保証していない挙動に依存している場合に問題が顕現しやすいようにするということを私は好ましく感じている。 プログラムの汎用的な部品を書くときには「こう動くべきだ」という想定の他に動いてはいけない場合についての配慮が必要だということは常々思っていて、何度かそのことを記事にしてきた。

問題があるなら早めに判明すべきで、早めに判明しやすい設計は良い設計だと思う。

Document ID: 3895e4797a586a74f9ae676af458279b

言語仕様と処理系仕様と実装と

プログラミング言語では仕様で保証されている挙動とそうでない挙動がある。 単に検討されなかった部分について記述が漏れている場合もあるし、実装の裁量で効率的な方法を選択できるようにあえて規定しない場合もある。 たとえば Scheme では map 手続きがリストを評価する動的順序は規定していない。 これは仕様書に「未規定である」と明記されているので意図的に規定を避けたものだと思う。 処理系によって、あるいは同じ処理系でも状況によって適当な順序を選択することが可能なのだ。 副作用を生ずる手続きを map に渡した場合には状況によって異なる結果が生じることが有り得る。

とは言うものの、現実的には評価順序はリストの先頭からか末端からかのどちらだろうし、仕様に熟知しているのでなければリストの先頭側から評価するものと期待してしまうというのもそう不自然ではないだろう。 初心者がそういうコードを書いているのを見たことは何度かある。 また、 Gauche では map はリストの先頭側から処理することがドキュメントに明記されているので Gauche に限ってはそれを期待することが出来る。 処理系の仕様として保証しているわけだ。

その他、手元の環境にインストールしている処理系で map がリストのどちら側から処理するか試してみた。 以下はリストの先頭側から処理した処理系だ。

  • Sagittarius 0.7.4
  • Chicken 4.11.0
  • Scheme48 1.9.2
  • Ypsilon 0.9.6-trunk/r506
  • Mosh 0.2.7
  • Rhizome/pi 0.57
  • Larceny 0.99
  • Guile 2.0.11
  • Racket 6.5
  • Foment 0.4

それぞれのドキュメントを確認していないので保証しているかどうかまでは知らないが、これらは実際にリストの先頭側から処理する実装になっている。

以下のように短かいリストで試してみただけなので複雑な状況で挙動が変わる場合も有るかもしれないが。

(map display '(1 2 3 4 5))

つまり、ある挙動を保証しているのは言語仕様か、処理系の仕様か、実装が結果的にそうなっているだけなのかという種類があって、その言語で書いたプログラムが動いているのも実は偶然でしかないという場合は意外にあると思う。 個別の処理系の方針について詳細を常に把握するのは難しいが、書いたプログラムをたまには別の処理系で動かしてみたりするくらいはした方が良いと思う次第である。

余談だが、上記の確認において Husk 3.19.2 は map がリストの末端側から処理した。

更に Chez Scheme 9.4.1 は 2 個ごとに入れ替わるような順序になった。 53412 というような順序である。 もちろんこれでも言語仕様には違反しないが、どうしてそういう選択をしたのかは興味深いのでいずれソースコードを確認してみようと思う。

Document ID: b939601ec40ec23a868fde033123cfd9

あるべき姿に

自分が書いたプログラムを見て、汚いなぁと思うことがある。 拡張するときに動いている部分にはなるべく手を付けないようにするのはありがちなことだが、そうすると拡張した部分に不格好な部分が出来てしまい、それが詰み重なると不整合だらけになるのだ。 それほど長く使うつもりがなかったものがいつまでも残ってしまう。

重い腰を上げてそういう負債の積み重ねをなんとかしようと思っても、それを作ったのは自分自身である。 最初に作ったときと同じ発想で設計しなおしてしまい、小手先の綺麗さが少し改善される程度にとどまってしまう。

そういったときにはインターフェイスになる部分から書いていくのが良いと思う。 自分が欲しい姿で部品の外側を作ってしまえばそれが構造を決める。 それでどうしても中身が汚くなってしまっても、不整合が積み重なるよりは良い。

プログラムを分割統治するにあたって抽象化の壁を作るのは重要なことだが、壁の向こうを知っているときにあえてそれを忘れるということも出来ないと綺麗な構造を保つのは難しい。 というよりは、壁の向こうを忘れていられるような壁を作らなければならない。

Document ID: f66b5cacc6c6fe1f9a5aa83b82911aa3

想定と拡張性

Regnessem というソフトウェアがある。

http://regnessem.osdn.jp/

これはインスタントメッセンジャではあるが、より厳密に言うとそれらの基盤となるもので、プラグインで機能を追加できる。 プラグインを大別すればユーザーインターフェイスと通信 (プロトコル) とその他に分けられる。 プラグインは必ずしも通信系のものだけではなく、たとえばノートパソコンの電池残量を通知するようなものもある。

私はとあるウェブチャットを Regnessem を通じてやりとりするためにプラグインを書いたことがあるのだが、そのときに RegnessemAPI に合わせるのに手間がかかった部分がある。 それはログインだ。 私が通信したかったウェブチャットには入室という仕組みがなく、名前と発言を入力すればすぐに発言できるものだった。 何の前触れもなく誰かが発言することがあったのである。

RegnessemAPI はセッション (プロトコルによってはチャンネルとか部屋などといわれる概念) を生成した後にそこへの参加者のアカウントを登録してから実際のメッセージのやりとりが始まるという手順を想定していたので、辻褄を合わせるにはプラグインの側で擬似的なログイン処理をするしかなかった。 発言するのが初めての人がいればその時点でログインしたとみなし、発言がない状態が続けばログアウト扱いとする具合である。

この場合はプラグインの側でどうにか出来たが、若干の無理をしたことにはかわりない。 フレームワークだとかライブラリの想定から外れてしまうと無理をせざるを得ない部分が出てくる。 フレームワークだとかライブラリは拡張性を確保しようとするなら、用途を充分に想定しないといけないな、ということを思った。

Document ID: dac77474f2f7a366afbad4185cd217e7

制度設計とトレードオフ

家庭用の風呂場や便所の扉に錠をかけられるようになっている場合がある。 しかし大抵は外側から錠を開けることが出来るようにもなっている。 十円玉を差し込んでひねるだけで簡単に開く。 これでは錠の役目をはたしていないではないかと思う人もいるかもしれないが、この錠は「使用中に誤って扉を開けないため」のものであって、その機能は充分に果す。 悪意を持って押し入る者を防ぐことを想定したものではない。 そもそも家の中に悪意を持った者がいたらもう駄目だろう。

では家の内部ではなく玄関扉はどうだろう。 玄関の鍵だって複製は簡単だ。 ホームセンターなどで複製を受付けている。 そして複製のために鍵を持ち込むにあたって身分を提示する必要がないし、審査もない。 あまりに簡単なので不安を感じるという意見は何度か目にしたことがある。

どうして身分を確認しないのかというと、持ち込まれた鍵がどこの鍵かを複製業者が知ってしまうと余分に複製しておいて窃盗に用いるということが出来てしまうからだそうだ。 実際にやるかどうかは問題ではなく、何かあったときにいちいち疑いを持たれるのも面倒な話だろう。 また、単純に数の問題でもある。 複製業者に鍵を持ち込む者が悪意を持っていたとして、それで起こる事件は一件に過ぎない。 複製業者が悪意を持てばかなり大きな規模のものになる。 事件の数が少なければ良いというものではないが、多いよりは良い。 そう考えれば鍵の複製を依頼するのに身分証明が不要という制度設計には妥当性があるとわかる。

制度に不安を感じたとき、その制度でなかった場合と比較して考えてみるとよい。 先日は機械の設計におけるトレードオフの話を書いたが、それは制度設計にも言えるわけだ。

Document ID: 142246b01950caab3d15952ccea42617

Chez Scheme で Foreign Interface を使ってみた

しばらく前に Chez Schemeオープンソース化された。

https://github.com/cisco/ChezScheme

Chez Schemeインタプリタ版である Petite Chez Schemeソースコードこそ公開されていなかったものの、以前から無料 (オープンソースではない) で利用できていたが、 Chez Scheme は Petite Chez Scheme よりもかなり高速なようだ。

しかし、私にとっては、速度はあまり重要ではない。 というより、 Petite Chez Scheme で充分すぎる速さだというべきか。 時間がかかる処理をすることがあまりないので劇的に体感する機会がないのだ。 たとえば二分かかる処理が一分になるなら大きな違いだが、二秒の処理が一秒になったからといってそれほどでもない。 (私の関心の外であるというだけで、技術的な高度さはまた別である。)

それよりも私が関心を持ったのは Foreign Interface だ。

http://www.scheme.com/csug8/foreign.html

基本的には Petite Chez Scheme と Chez Scheme とはほぼ互換性があるのだが、 Petite Chez Scheme で利用できなかった Foreign Interface が Chez Scheme では使える。 Foreign Interface は共有オブジェクト (Windows では DLL) の機能を呼び出すもので、 Scheme の外の世界への窓口となる。 処理系が用意しているものでどうしても機能が足りないときには C で書いて Foreign Interface で繋いでしまえば何でも出来る。 Foreign Interface があれば出来ることが一気に増えるわけだ。

そこで私は試しに HTTP 接続をやってみた。 Windows で Wininet を利用するものである。

(import (chezscheme))

(load-shared-object "Wininet.dll")

(define-ftype handle void*)

(define internet-open
  (foreign-procedure __stdcall "InternetOpenW"
                     (wstring unsigned-32 wstring wstring unsigned-32)
                     handle))

(define internet-open-url
  (foreign-procedure __stdcall "InternetOpenUrlW"
                     (handle wstring wstring unsigned-32 unsigned-32 handle)
                     handle))

(define INTERNET_OPEN_TYPE_DIRECT 1)
(define INTERNET_FLAG_RELOAD #x80000000)

(define internet-read-file
  (foreign-procedure __stdcall "InternetReadFile" (handle u8* unsigned-32 u32*)
                     boolean))

(define internet-close-handle
  (foreign-procedure __stdcall "InternetCloseHandle" (handle) boolean))

(define (http-get url bport)
  (let* ((hinternet
          (internet-open "sample" INTERNET_OPEN_TYPE_DIRECT #f #f 0))
         (hfile
          (internet-open-url hinternet url #f 0 INTERNET_FLAG_RELOAD 0)))
    (let ((vec (make-bytevector 1024))
          (vsize (make-bytevector 4)))
      (let loop ((r (internet-read-file hfile vec 1024 vsize)))
        (let ((s (bytevector-u32-ref vsize 0 (endianness little))))
          (unless (zero? s)
            (put-bytevector bport vec 0 s)
            (loop (internet-read-file hfile vec 1024 vsize)))))
      (flush-output-port bport))
    (internet-close-handle hfile)
    (internet-close-handle hinternet)
    (if #f #f)))

(display
 (utf8->string
  (call-with-bytevector-output-port
    (lambda(out)
      (http-get "http://example.com/" out)))))

Foreign Interface を使うときは些細な間違いで簡単にクラッシュするので、汎用的なライブラリを作るときは引数のチェックを厳しくした方が良いと思う。

Document ID: 4469def7a696bf182842bea7e01d5154