新しいCommon Lisp方言「CL21」を作ったので意見を募集します
昨晩、神の啓示か何か知りませんが、ふと思い立って新しいLisp方言を作りました。
ほとんどの機能はCommon Lisp互換なので「Common Lisp方言」と言うべきかもしれません。
CLerだけでなく、Common Lispをあまり書いたことがない人やそれ以外の言語を使っている方の意見も伺いたいのでぜひ最後までご覧ください。
名前は「Common Lisp in the 21st Century」の略で「CL21」です。
特徴
- よりオブジェクト指向に
- 関数型プログラミング機能を仕様に追加 (
compose
やcurry
など) - MOPを仕様に含む
CL21のチュートリアル
Common Lispと似ている部分が多いので、わかりやすい異なる部分をいくつか紹介します。
Hello, World!
まずはHello, Worldから。
(write-line "Hello, World!") ;-> Hello, World! ;=> "Hello, World!"
普通ですね。
文字列を繋げたい場合はconcat
を使います。
(write-line (concat "Hello, " "John McCarthy")) ;-> Hello, John McCarthy ;=> "Hello, John McCarthy"
ハッシュテーブル
次はハッシュテーブル。新しいハッシュの作り方はCommon Lispと同じです。
(defvar *hash* (make-hash-table))
ハッシュから値を取り出すにはgetf
を使います。まだ空なのでNIL
が返ってきます。
(getf *hash* :name) ;=> NIL
値を代入するにはsetf
を使います。
(setf (getf *hash* :name) "Eitarow Fukamachi") ;=> "Eitarow Fukamachi" (setf (getf *hash* :living) "Japan") ;=> "Japan" (getf *hash* :name) ;=> "Eitarow Fukamachi"
ハッシュテーブルを属性リスト (プロパティリスト aka "plist") に変換するにはcoerce
が使えます。
(coerce *hash* 'plist) ;=> (:LIVING "Japan" :NAME "Eitarow Fukamachi")
CL21ではgetf
やcoerce
がメソッドとして定義されており、さまざまな型を取ることができるようになっています。
ベクタ
次にベクタ。こちらも作り方はCommon Lispと同じです。
(defvar *vector* (make-array 0 :adjustable t :fill-pointer 0))
長さ可変のベクタに要素を追加するにはpush
が使えます。
(push 1 *vector*) ;=> 0 (push 3 *vector*) ;=> 1 *vector* ;=> #(1 3)
各要素の値にアクセスするにはnth
を使います。
(nth 1 *vector*) ;=> 3
値をセットするにはsetf
を使います。
(setf (nth 1 *vector*) "Hello, Lispers") ;=> "Hello, Lispers" *vector* ;=> #(1 "Hello, Lispers")
pop
で最後の値を取り出せます。
(pop *vector*) ;=> "Hello, Lispers" (pop *vector*) ;=> 1
ループ
最後の例は繰り返し(ループ)。
Common Lispにはloop
という何でもできるミニ言語がありますが、CL21ではもう少し汎用的で一貫性のあるループ構文をいくつか用意していと思っています。
たとえば、while
とuntil
です。条件式が真や偽である間だけループを繰り返します。
(let ((x 0)) (while (< x 5) (princ x) (incf x))) ;-> 01234 ;=> NIL
もし条件式の返り値をループ内で使いたい場合はwhile-let
が使えます。
(let ((people '("Eitarow" "Tomohiro" "Masatoshi"))) (while-let (person (pop people)) (write-line person))) ;-> Eitarow ; Tomohiro ; Masatoshi ;=> NIL
さらに追加されたループ構文がdoeach
です。Common Lispのdolist
と似ていますが、リストだけでなくすべてのシーケンス (ベクタなど) に使える点が異なります。
(doeach (x '("al" "bob" "joe")) (write-line x)) ;-> al ; bob ; joe ;=> NIL
loop ... collect
のようにループ内で値を取り出したい場合はcollecting
マクロが使えます。
(collecting (doeach (x '("al" "bob" "joe")) (when (> (length x) 2) (collect x)))) ;=> ("bob" "joe")
実装は?
Common Lisp上で実装したため、お使いのSBCL, Clozure CLなどで動くと思います。(一晩で作れたのはそのせい)
インストール
手元で試すには新しくQuicklispのdistをインストールし、ql:quickload
します。
(ql-dist:install-dist "http://qldists.8arrow.org/cl21.txt") (ql:quickload :cl21)
自分のアプリケーションで使う場合はasdファイルの依存ライブラリに:cl21
を追加し、以下のようにパッケージを定義します。
(defpackage myapp (:use :cl21)) (in-package :myapp)
:use :cl
ではなく :use :cl21
にするのがポイントです。
デザインポリシー
CL21は以下の3点を念頭に、機能的な意味での「Common Lispのスーパーセット」を目指してデザインしています。
- 既存のCommon Lispのアプリケーションと完全に問題なく動作する
- Common Lispが持つ機能は (ほぼ) すべて継承する
- 速度を意識しない
今存在するCommon Lispのライブラリやアプリケーションと協調して問題なく動くことは最重要です。こうすることで既にあるCommon Lispのライブラリ資産を使うことができます。ClojureがJVM上で動くからJava資産が使えるのと一緒ですね。
一番最後の「速度は意識しない」という点はCommon Lisperにとって必ずしも受け入れられるものではないことは知っています。
Common Lispは実用的な言語ですから、Cのように高速なプログラムを書くことができます。高速さがCommon Lispの価値の一つなのに、それを失うことは愚かなことなのかもしれません。
けれど、言語自身の拡張性も僕は同様に大事だと思っています。
たとえば、ハッシュテーブルのようなクラスを作りたいと思ったとき、今のCommon Lispではhash-tableを継承することはできない (built-in-classなので) ので、仕方なくstandard-classを継承したものを作りますが、gethash
はできないし、明らかにCommon Lispが持つハッシュテーブルに似せることはできません。
CL21ではgetf
が汎用メソッドになっているため、独自クラスにgetf
を定義することが可能です。equal
やequalp
も独自のクラスに対して定義することができます。
これらにより、より言語に近いプログラムを書くことができます。
Common Lispとの協調
僕も速度がまったく重要ではないと思っているわけではありません。もし本当に速度が重要な処理であれば、代わりにgethash
を使ったり、cl:equal
を使ったりして実行時の型チェックをしないようにすればいいだけの話です。
また、僕はいくつかのCommon Lispライブラリを公開していますが、それらをCL21で書きなおす気はありません。今後もCommon Lispライブラリを書くならCommon Lispで書くと思います。
その一方で、知った人間しか使わないようなWebアプリケーションを書くときにはCL21を使います。そのほうが読みやすく簡単にプログラムが書け、プロトタイプを短時間で作れると思うからです。
名前が「21世紀の」とかついてるから無駄に敵を作ってしまった感あるけど、完全に置き換えようとするわけではなく少なくともしばらくは協調していけばいいかなと思っています。
おわりに
ということで何か意見があれば@nitro_idiotまで。ブログ公開前にRedditにも貼られてしまったので、そちらで議論していただいても構いません。
早速「メソッドにしたらRubyみたいな遅い言語になっちまうだろーが」ってコメントがついていて面白いですね。