八発白中

技術ブログ、改め雑記

新しいCommon Lisp方言「CL21」を作ったので意見を募集します

昨晩、神の啓示か何か知りませんが、ふと思い立って新しいLisp方言を作りました。

ほとんどの機能はCommon Lisp互換なので「Common Lisp方言」と言うべきかもしれません。

CLerだけでなく、Common Lispをあまり書いたことがない人やそれ以外の言語を使っている方の意見も伺いたいのでぜひ最後までご覧ください。

名前は「Common Lisp in the 21st Century」の略で「CL21」です。

特徴

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ではgetfcoerceがメソッドとして定義されており、さまざまな型を取ることができるようになっています。

ベクタ

次にベクタ。こちらも作り方は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ではもう少し汎用的で一貫性のあるループ構文をいくつか用意していと思っています。

たとえば、whileuntilです。条件式が真や偽である間だけループを繰り返します。

(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 Lispdolistと似ていますが、リストだけでなくすべてのシーケンス (ベクタなど) に使える点が異なります。

(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のライブラリ資産を使うことができます。ClojureJVM上で動くからJava資産が使えるのと一緒ですね。

一番最後の「速度は意識しない」という点はCommon Lisperにとって必ずしも受け入れられるものではないことは知っています。

Common Lispは実用的な言語ですから、Cのように高速なプログラムを書くことができます。高速さがCommon Lispの価値の一つなのに、それを失うことは愚かなことなのかもしれません。

けれど、言語自身の拡張性も僕は同様に大事だと思っています。

たとえば、ハッシュテーブルのようなクラスを作りたいと思ったとき、今のCommon Lispではhash-tableを継承することはできない (built-in-classなので) ので、仕方なくstandard-classを継承したものを作りますが、gethashはできないし、明らかにCommon Lispが持つハッシュテーブルに似せることはできません。

CL21ではgetfが汎用メソッドになっているため、独自クラスにgetfを定義することが可能です。equalequalpも独自のクラスに対して定義することができます。

これらにより、より言語に近いプログラムを書くことができます。

Common Lispとの協調

僕も速度がまったく重要ではないと思っているわけではありません。もし本当に速度が重要な処理であれば、代わりにgethashを使ったり、cl:equalを使ったりして実行時の型チェックをしないようにすればいいだけの話です。

また、僕はいくつかのCommon Lispライブラリを公開していますが、それらをCL21で書きなおす気はありません。今後もCommon Lispライブラリを書くならCommon Lispで書くと思います。

その一方で、知った人間しか使わないようなWebアプリケーションを書くときにはCL21を使います。そのほうが読みやすく簡単にプログラムが書け、プロトタイプを短時間で作れると思うからです。

名前が「21世紀の」とかついてるから無駄に敵を作ってしまった感あるけど、完全に置き換えようとするわけではなく少なくともしばらくは協調していけばいいかなと思っています。

おわりに

ということで何か意見があれば@nitro_idiotまで。ブログ公開前にRedditにも貼られてしまったので、そちらで議論していただいても構いません。

早速「メソッドにしたらRubyみたいな遅い言語になっちまうだろーが」ってコメントがついていて面白いですね。