goo blog サービス終了のお知らせ 
見出し画像

Retro-gaming and so on

RE: プログラミング学習日記 2023/02/22〜

星田さんの記事に対するコメント。

> 話題その2 マクロ学習

まぁ、十中八九、「自分でマクロを書こう」と言うネタは思いつかないんだよ(笑)。
逆に、「マクロがある程度書けるようになると」今度は不必要に何でもマクロにしようとする(笑)。「マクロ書きたい病」になるんだ(笑)。
幸いな事に、標準Schemeにあるsyntax-rulesはそんなに複雑で高度なマクロは書けない。結果「抑制が効いた」使い方をせざるを得なくなる。

一番あり得る「マクロの使い方」は、ANSI Common Lisp組み込みマクロの移植だろう。ある程度なら(と言うか、ハッキリ言ってloopマクロ以外なら)移植が可能だし、「移植したい」と思うようなマクロがANSI Common Lispにはてんこ盛りだ。
従って、ANSI Common Lispの仕様書であるCommon Lisp HyperSpec(※1)の読み方をある程度知っておく必要がある。

ここにも書いたけど、CLHS(Common Lisp HyperSpec)のtypecaseのページのケツの辺りには次のようなNoteが書かれている。


これは単純に、マクロの定義式なんだ。
つまり

(typecase test-key
 {(type form*}*)

と言う式を

(let ((#1=#:g0001 test-key))
 (cond {((typep #1# 'type) form*)}*)) 

に変換しろ、って言っている。この変換式さえ書ければ自分でtypecaseと言うマクロを書く事ができる、って言ってるわけだ。
ちと見慣れない記号もあるけど(#1=#:g0001とか・※2)、取り敢えず重要なのはアスタリスク(*)だ。これは「以下同文」を表し、Schemeのsyntax-rulesで言うと...(ellipsis)と同じ意味だ。
つまりこれだけの情報で、

(define-syntax typecase
 (syntax-rules ()
  ((_ test-key
   (type form ...) ...)

と書き始めればいい、って事が分かる。上のCLHSでの定義と良く見比べてみよう。
また、結果、変換後にせよ、typepが良く分からん関数だ、って前提だろうと、単純にガワとしては、

(define-syntax typecase
 (syntax-rules ()
  ((_ test-key
    (type form ...) ...)
  (let ((|#1#| test-key))
   (cond ((typep |#1#| 'type) form ...) ...)))))

とSchemeでは「書ける」と言う事を示している・・・ある意味「これで終了」なんだよ。マジで。
良くCLHSでのtypecaseの定義と見比べてみよう。単に「定義式通りに置き換えただけだ」と言うのが分かるだろう。

と言う事は、だ。typepと言う関数は形式的に見ても、「型」を指すtypeと言うシンボルとtest-keyが持ってる「型」を指すシンボルが同じかどうか判定してるだけなんじゃないか、って予想が付く筈だ。
シンボル同士の等価性はSchemeだとeq?で判定が付くし、また「どんな型なのか」調べる為にRacketのライブラリであるdescribeを導入した。
従って、単純に言うと、

(define-syntax typecase
 (syntax-rules ()
  ((_ test-key
    (type form ...) ...)
  (let ((|#1#| test-key))
   (cond ((eq? (variant |#1#|) 'type) form ...) ...)))))

で最低限の機能は実現できる、って意味になる。


また、散々っぱらcasevariantの組み合わせを使った経験があるんなら、「マクロの展開型」を次のように書けばもっと短く書ける、って事も想像が付くだろう。

(define-syntax typecase
 (syntax-rules ()
  ((_ test-key
   (type form ...) ...)
  (case (variant test-key) ;; 手書きプログラムの経験を活かして展開型を書く
   ((type) form ...)
   ...))))

基本的には、これだけ、の問題であってアタマを使う必要がない話って事になる。

マクロは不完全でも使い方さえ気をつければ問題なく動くだろう。
また、フツーの「関数」と同様、使ってるウチに不具合を見つけたらそれを修正しつつ「育てていく」と言うのが私用ライブラリでの使い方だ。

ただ、このマクロの問題はマクロのスタイルそのものには立脚してない。
むしろ、外部から導入したdescribevariant関数の方に問題がある。
平たく言うと、マニュアルの通りに動いていない。


マニュアルではこう書いてるが、実際実行してみると、



と若干違った結果が返ってきてる。
特に問題なのが、まずは数値の扱いだ。


どんな数値を入れようがsimpleが返ってくる(笑)。
これでいいのか悪いのか、って言うのは人に依るが、一般的には「困ったちゃん」だろうな。

もちろん、Schemeにはsimple型、なんつーのは無い。これはRacket独特の定義だ
どうやら、過去のPLT Scheme及びRacketではキチンと「型名」を返してたみたいだが、Racket内部の仕様変更の際にこの不具合が生じたようだ。
Racketで言うSimple Valueはマニュアルによると次のデータを含むようだ。

  • 数値
  • 真偽値
  • 文字列
数値は上で見た。
真偽値はその通りなんだけど、文字列はvariantは判定してくれる。


また、他にも空リストや文字もsimpleと判断される。



あとは、リストはペアかどうかしか判別しない。



他にも色々とあるかもしんないが、主だって見つけたのはこんなカンジだ。
これをどう帳尻が合うように実装すべきなのか・・・そう、これはマクロの問題と言うより、ANSI Common Lispのtypepに比する関数をどう書くのか、と言う問題だ。

以前書いた通り、仕様上、Schemeは「データ型のヒエラルキー」を持たない。直交性のある(※3)型は次のようにしか定義されていない。



ここで問題なのは、例えばnumber?で#tだと判定された数は、オブジェクト指向的に言うとそのサブクラス(例えば実数であるとか複素数であるとか)でも真偽判定が当然生じて、それによる「型」が概念的に存在する辺りだ。
しかし、Schemeではそういう「サブクラスの構造」を定義してないし、Racketも実装してない。
故にそういう「型判定」を無矛盾なく揃えるのがユーザー側から言うとかなり面倒なんだ。
よって、実際は「困ったらコードを見直して修正する」必要が出てくるだろう。
また、コンピュータサイエンス的には「探索」の問題とすり替えられる(※4)。ただし、ここではその実装がメンド臭いんで、条件分岐で凌ごう。
一例としては次のようなtype?関数の実装が考えられる。



例えばvariantがsimpleを返した際に、それがnumber?を満たすのか、null?を満たすのか、char?を満たすのか、boolean?を満たすのか調べる。
そして例えば「数の型」を調べたい際には、与えられたtype-specifierのシンボルがそれに一致してるかどうか調べる。一致しなかった場合、その節から更に深く潜っていくだけ、だ。
この実装だとANSI Common Lispが提供しててSchemeで互換性がある段階まで、で今のトコ止めている。SchemeにはANSI Common Lispでは存在しない、例えば与えられた数が無限大かどうか調べる述語まで用意してるけど、「ヒエラルキーのどこにそれが存在すべきか」と言うのがハッキリしない。
また、複素数にも「正確数」「不正確数」の概念があるが、そこにもツッコまないで止めておいた。なんか不都合が生じた際に追加すればいいだけだろうし、色々修正が大規模に必要になるなら、データ型のヒエラルキーを設計した方が早いかもしんないからね。あるいはdescribeと言うライブラリが修正されるかもしれんし。
いずれにせよ、この問題で大変なのは「マクロを作る事」より「マクロで使う関数の設計」の方が難しい、って話だ。

なお、上のマクロでelseを使いたい、って際にはちと修正が必要になるが、condcaseの仕組みさえ分かっていればその辺は大した問題にはならないと思う。

参考までに、今回のRacket版typecaseのソースコードはこちらから。

※1: 厳密に言うとCommon Lisp HyperSpecはANSI Common Lispの仕様書ではない。が、限りなく仕様書に近い。
ANSI(米国国家規格協会)はJIS(日本産業規格)と同様に「規格書を販売」してる。
しかしあまりにデカいANSI Common Lispの仕様書は買うのも大変、持つのも大変、そして読むのも大変、と言う困ったちゃんになってしまった。
そこで、ネットが一般に広がり始めた1996年頃、「ANSI Common Lisp仕様書を、そのままじゃないにせよ簡単に閲覧出来ないか?」と商用Common LispベンダーであるLispWorksがANSIに許諾を求めて、仕様書を若干コンパクトにまとめなおし、ネット上でフリーで閲覧可能なように整えてくれたわけだ。これがCommon Lisp HyperSpecだ。
なお、「HyperSpec」の「Hyper」っつーのは、要するに「HTMLで書いた文書だ」と主張してるだけ、だ。言い換えるとそこのHyperはHyper Text Markup Language(HTML)のHyperだ。
今じゃこうやって「Hyper Textですよ」と主張なんざしないが、1996年辺りだと、こういう主張はまだダサくなくてイケてたんだ(笑)。

※2: #1=#:g0001っつーのは困った表現だけど、Scheme内ではvarだろうとvalだろうと何でもいい。要するに「変数の名前」を表している。
ANSI Common Lispのマクロは強力なんだけど非常に危険で、Schemeの用語で言うと「衛生的ではない」。
ANSI Common Lisp内で「フツーの変数名を使う」と、場合によってはその書いたマクロの「外側に置いた」変数名と「偶然にも一致」してしまって、その外側にある変数を参照しちまったり、あるいは書き換えちまう、とか言う「事故」が起こり得る。
そこでANSI Common Lispでは「プログラム内に書かれたどんな変数名とも違う事が保証される」変数の自動生成機能があり、それをgensymと言う。#1=#:g0001っつーのは、そこの変数名はgensymで生成された、と言う提示だ。
この辺のANSI Common Lispに於ける変数補足の問題に付いて詳しく知りたければ、ポール・グレアムのOn Lispを参照の事。

※3: Orthogonal。もともとは数学用語で言う「直交性」から来てるが、あいにくコンピュータサイエンス上はハッキリとした定義がない、言わばバズワードっぽい使い方がされる。
ニュアンスとしては「互いに独立した」と言うような意味になるが、厳密な意味ではないし使い方でもない。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「RE: プログラミング学習日記」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事