y2q_actionman’s ゴミクズチラ裏

内向きのメモ書きを置いてます

Allegro Common Lisp の Modern Mode で読めなくなるコードとそれへの対処

Allegro Common Lisp の Modern Mode とは

ANSI Common Lisp のリーダーは、 初期状態では symbol 中の小文字を大文字にして読む。 Allegro Common Lisp はこの挙動を ANSI Mode と称している。

CL-USER> (symbol-name 'hoge)
"HOGE"

CL-USER> (symbol-name 'HoGe)
"HOGE"

CL-USER> (eq 'hoge 'HoGe)
T

この挙動は ANSI 標準なので当然のように使われている一方、 symbol に大文字小文字を混在させたい場合に | などでエスケープする必要があって面倒でもある。これで困る状況の例として Franz の文書 に載っているのは、 FFIWindows API を呼ぶときに関数名をそのまま使おうとしても全部大文字になってしまって見づらい、というもの:

(defun call-windows ()
  (CreateDIBitmap ...))

;; 以下と区別がつかず、 backtrace でも以下の関数名が表示される。
(DEFUN CALL-WINDOWS ()
  (CREATEDIBITMAP ...))

そういう機会を見越してか、 Allegro Common Lisp には "Modern Mode" というものがある。 ANSI Mode に対して、 Modern Mode では以下が変化する。

  • 初期状態で readtable-case :preserve とする。
  • 組み込みの symbol が小文字で定義される。
cl-user> (symbol-name 'hoge)
"hoge"

cl-user> (symbol-name 'HoGe)
"HoGe"

cl-user> (eq 'hoge 'HoGe)
nil

Modern Mode では、大文字小文字を区別した symbol が作られるので、それを生かしたコーディングがやりやすくなる。

筆者は、 Allegro Graph の Lisp client を試用しているのだが、こいつは Modern Mode でないと動かない。 Modern Mode による大文字小文字の区別と !リードマクロにより、 triple のデータをコード中にいい感じに表現できるようになっている。

以下に AllegroGraph 8.3.1 Lisp Quick Start の例 を引用する:

> (add-triple !ex:Mammal !rdf:type !owl:Class)
1
> (add-triple !ex:Dog !rdfs:subClassOf !ex:Mammal)
2
> (add-triple !ex:Fido !rdf:type !ex:Dog)
3
> (add-triple !ex:Fido !ex:has-owner !ex:John)
4
> (add-triple !ex:Company !rdf:type !owl:Class)
5
> (add-triple !ex:CommercialCompany !rdfs:subClassOf !ex:Company)
6
> (add-triple !ex:Franz !rdf:type !owl:CommercialCompany)
7
> (add-triple !ex:John !ex:works-at !ex:Franz)
8
> (add-triple !ex:Franz !ex:has-product !ex:AllegroGraph)
9

Modern Mode で既存のコードやライブラリを読む

筆者は Modern Mode を使ってみているが、やはり Allegro Common Lisp の機能だけでは不足する場面で外部のオープンソースのライブラリを使いたくなる。 外部のライブラリは当然 ANSI Mode で書かれているので読めるのかどうか心配になるが、多くの Lisp コードは小文字で書かれているので、大抵はそのまま読んで使うことが出来た。

一方で手直ししないと使えないライブラリも当然あった。

この記事では、それらの手直ししないと Modern Mode では読めないコードやライブラリの事例とその修正事例を紹介しようと思う。

Modern Mode で読めなくなったコード例とその修正

Tを大文字にしがち

筆者が書いたコードで、成功したら t を返す関数を以下のようにしていた:

(defun hoge ()
  (progn ...) ; 何かの処理
  T)          ; 成功したら T

ここで返すものを T と書いていた。 Modern Mode だと小文字の t はあっても大文字の T はないのでエラーになる。 この場合は自分で書いたコードだったので、以下のように書きなおした:

(defun hoge ()
  (progn ...) ; 何かの処理
  t)          ; 成功したら t

cl-csv の例

自分の書いたコードなら適当に手直しすればいいが、例えば Quicklisp で引っ張りたい外部のライブラリは面倒になる。

例えば cl-csv に、 大文字 T を返すコード が見つかった。

自前で fork して何やらやってもいいがそれも管理が面倒なので、 cl-csv を load する前に以下のようなコードを load して誤魔化すことにした:

(eval-when (:compile-toplevel :load-toplevel :execute)
  (when (find-package "common-lisp")
    (unless (find-package :cl-csv)
      (make-package :cl-csv)
      (setf (symbol-value (intern "T" (string '#:cl-csv)))
            cl:t))))

やってること:

  1. Modern Mode だと (find-package "common-lisp") が true になるので Modern Mode を判定
  2. cl-csv package を勝手に作る。
  3. 勝手に作った cl-csv package の cl-csv::Tcl:t を持たせる。

これをやってから cl-csv を読むと、 Allegro Common Lispいい感じに package 定義をマージしてくれて、なんとかしてくれる。 *1

固有名詞は大文字にしたい例

筆者が書いていたコードで以下のようなものがあった:

(defparameter *AWS-access-key-id* nil)

(let ((*aws-access-key-id* "..."))
  (progn ...)) ; なんかの処理

見ての通り AWS に関連する変数を名付け、それを使っている。この時、変数を名付けるときは 「AWS は "AWS" で "aws" じゃないよなあ」と大文字にしているのに、それを使ってコードを書くときは 「いやー小文字で書くでしょ」と気分が変わっていた?のかなと思う。 ANSI Mode だと問題ないが、 Modern Mode だと大文字小文字が違うと区別されてしまうのでエラーになってしまう。

これについては、せっかくなので変数定義時の方に統一することにした。

(defparameter *AWS-access-key-id* nil)

(let ((*AWS-access-key-id* "..."))
  (progn ...)) ; なんかの処理

型の名前は大文字にしがち

他にこんなコードがあった:

    (concatenate '(SIMPLE-ARRAY (UNSIGNED-BYTE 8) (*)) ...)

型の名前はなんか目立ってほしいし大文字で残してあるのも分かる。

でも Modern Mode だと通らないので小文字にした:

    (concatenate '(simple-array (unsigned-byte 8) (*)) ...)

外部由来の定数 (cl+ssl)

cl+sslPull Request 93 では、 SSL 由来っぽい大文字の定数を小文字に統一する変更がされている。

こういう外部から取り込んだ定数とかはそのまま大文字で置いておきたい気持ちになるし、 ANSI Mode ならそれで問題になりにくいので放置されがちと思う。

上記 Pull Request は「 :invert だと読めないので直した!」というもので「どうしてそれを?」とも思うけど、入っていてくれて筆者にとってはありがたい。 *2

keyword と *features*

以下のコードがこけていた:

(defun hoge ()
  #+ALLEGRO(progn ...)
  #-ALLEGRO(error "Unsupported.")
  )

keyword も symbol の一種なので当然 Modern Mode の影響を受ける。 これはすぐに気付いて適当に直していたのだが、 *features* の中身も keyword であることに当初は気づいていなかった。

ANSI Mode では features に大文字の :ALLEGRO がいるので上記のコードで動くが、 Modern Mode だと小文字の :allegro しかいないので動かなくなってしまう。 小文字に統一して解決した。

(defun hoge ()
  #+allegro(progn ...)
  #-allegro(error "unsupported.")
  )

外部ライブラリで #+ で分岐するコードは大抵小文字で書かれているようで、今のところこれに引っかかった外部のコードはない。 もしあったら、そいつのためだけにあらかじめ *features* をいじらないといけないと思うので面倒そう。

find-symbol や find-package

ANSI Mode を前提とするとき、 symbol 名は大文字でそれを前提に find-symbol するコードを書いていた:

(in-package :cl-user)

(defvar *some-variable* t)

(find-symbol "*SOME-VARIABLE*" :cl-user)

これは当然ながら Modern Mode では動かない。対処法は Allegro CL Modern Mode の文書 にある通りで、 適当な symbol を作ってその名前を取ればよい:

(find-symbol (string '#:*some-variable*) :cl-user)

さらに、何かのパラメータで find-symbol するときは「大文字に変換しなきゃ」ということで string-upcase を通したりしていた:

(defun find-symbol-in-cl-user (string)
  (find-symbol (string-upcase string) :cl-user))

(find-symbol-in-cl-user "*some-variable*")

これについては、 ANSI Mode と Modern Mode の両対応のために以下のようにしてみた:

(defun find-symbol-in-cl-user (string)
  (or (find-symbol string :cl-user)
      (find-symbol (string-upcase string) :cl-user)))

以上の find-symbol の引数についての議論は、 find-package にも当てはまる。

intern

ANSI Mode を前提とするとき、 symbol 名は大文字であり、大文字に統一しないとエスケープが必要になったりする。そうなると面倒なので symbol を intern で作るときには string-upcase を通していた:

(defun make-keyword (string)
   (intern (string-upcase string) :keyword))

しかし Modern Mode だと邪魔になる。以下のように書き直した.

(defun make-keyword (string)
   (intern (if (find-package "common-lisp") ; Modern Mode ?
               string
               (string-upcase sub0))
           :keyword))

また、マクロなどで symbol 名を付けて intern するときも大文字を仮定していた:

(defun make-tmp-func-name (n)
  (intern (format nil "TMP-FUNC-~D" n)))

これも大文字小文字を反映するようにした:

(defun make-tmp-func-name (n)
  (intern (format nil "~a-~d" (string '#:tmp-func) n)))

cl-json の camel case 変換の例

JSON を読むライブラリである cl-json には、Camel Case をいい感じの Lisp symbol 名に変換する機能 が備わっている:

cl-user> (json:camel-case-to-lisp "camelCaseSymbol")
"CAMEL-CASE-SYMBOL"

cl-user> (json:lisp-to-camel-case "CAMEL-CASE-SYMBOL")
"camelCaseSymbol"

しかしこれらは当然 ANSI Mode を前提に書かれていて、Lisp 側は大文字にするように書かれている。

幸い、 cl-json は Camel Case 変換の関数を差し替えられるように出来ていたので、 Modern Mode 対応としてはそれを使うことにした。以下は現在の仮実装で string-downcase して誤魔化すもの。そのうちちゃんとしたい:

(defun camel-case-to-lisp-downcase (string)
  (string-downcase (json:camel-case-to-lisp string)))

(defun lisp-downcase-to-camel-case (string)
  (json:lisp-to-camel-case (string-upcase string)))

(setf json:*json-identifier-name-to-lisp* 'camel-case-to-lisp-downcase)
(setf json:*lisp-identifier-name-to-json* 'lisp-downcase-to-camel-case)

write したものを read するとき (cl-unicode)

Lisp だと、お手軽なデータ保存方法として「一旦 write して後で read する」をやることがある。

cl-unicode がこれに該当していて、 cl-unicode/build という defsystem で "lists.lisp" "hash-tables.lisp" "methods.lisp" を write し、 cl-unicode 本体ではそれを load する構成になっている。

ここで、ANSI Mode で大文字のみを使って write したものを Modern Mode で read して使おうとしても当然ながらこけてしまう。 Quicklisp で load しようとするとこれに引っかかって、 load 時にエラーになってしまうことがある。

対処法としては cl-unicode を build し直すと当該ファイルが作り直されるので、 Modern Mode で以下を実行すればよい:

(asdf:load-system :cl-unicode/build)

まとめ

Allegro Common Lisp の Modern Mode を使う機会があって使ってみたところ、意外と大文字に統一する仕様に乗っかってたんだな・・と思った。

参考資料

追記: Allegro CL Case mode について

この記事を書いた後、 「Allegro CL には in-case-mode というマクロがあって、これを使えば Modern mode で ANSI mode のコードを読んだり、その逆が出来たりする」と教えてもらった。

ちょっと試したところ、上記の cl-csv や cl-unicode の load 時の問題は in-case-mode 指定で回避できそうとわかった。

AllegroGraph は Modern mode べったりなのでそれを触るコードは Modern mode を使わざるをえないとはいえ、外部の ANSI mode のコードを読むのは上に書いたような変更はしないでも行けそう。

参考資料

*1:Allegro Common Lisp 以外だとなんかエラーになるかなと思うが気にしない.

*2:Pull Request を読んでいたところ、 Modern Mode 対応が動機 だった。先駆者ありがたい・・