八発白中

はてなブログに引越しました。

軽量なCommon LispのDBライブラリ「datafly」を作りました

Common Lispのデータベースライブラリというか、O/Rマッパーとしては3ヶ月前に僕が作ったIntegralがあります。

IntegralはCLOSやMOPなどのCommon Lispの魔術を余すこと無く使い、拡張性や高度なマイグレーション機能もあるライブラリとして他の追随を許しません。

ただ、すべてのアプリケーションでO/Rマッパーのような機能が必要なわけではないでしょう。抽象化レイヤーを薄く保って、極力コントローラブルにしたいという要望もあります。

今回紹介する「datafly」はそういった要求を満たす軽量なDBライブラリです。

dataflyの思想

一般的なO/Rマッパーでは、データベースの「テーブル」と、プログラム言語の「クラス定義」が一対一対応しています。この大きな前提のおかげでデータベースを抽象化でき、まるでクラス定義が(半)永続化しているように錯覚させてくれます。

ただし、そこにはトレードオフがあります。

O/Rマッパーはその性質上データベースやSQL発行を表向き見えなくするものなので、コストのかかるSQL発行が行われているときに気づきづらくなります。

その点、dataflyは逆の思想に基づいています。

dataflyでは暗黙のSQLの発行を行いません。マクロを除く黒魔術は使いません。透明性を重視し、アプリケーションごとの最適化を行いやすくコントローラブルな状態に保ちます。

機能

上述の通り、dataflyはO/Rマッパーではありません。たとえば、dataflyは以下のようなことはしません。

dataflyがやるのはこんなことです。

  • DBコネクション管理
  • CL-DBIをラップして扱いやすく
  • 結果を構造体(Structure)にマッピング
  • inflate

CLOSの標準クラスではなく構造体を使うのでいくらか効率も良いはずです。

クイックスタート

構造体(Structure)へのマッピング

dataflyではSQLの発行方法としてretrieve-oneretrieve-allexecuteの3つの関数があります。すべて引数としてSxQLのクエリオブジェクトを受け取ります。

たとえば、SELECT文を投げて、結果を1つ返して欲しいときはretrieve-oneを使います。

(retrieve-one
  (select :*
    (from :user)
    (where (:= :name "nitro_idiot"))))
;=> (:ID 1 :NAME "nitro_idiot" :EMAIL "nitro_idiot@example.com" :REGISTERED-AT "2014-04-14T19:20:13")

返り値はプロパティリストです。

キーワード引数の:asを指定すると、結果を指定した構造体(Structure)として返します。

(defstruct user
  id
  name
  email
  registered-at)

(retrieve-one
  (select :*
    (from :user)
    (where (:= :name "nitro_idiot")))
  :as 'user)
;=> #S(USER :ID 1 :NAME "nitro_idiot" :EMAIL "nitro_idiot@example.com" :REGISTERED-AT "2014-04-14T19:20:13")

(user-name *)
;=> "nitro_idiot"

この例ではテーブル名と構造体の名前が同じですが、同じである必要は全くありません。dataflyはテーブルとクラスが一対一対応ではないからです。

この自由さは、架空のテーブル――たとえばJOINした結果――などを構造体として扱いたいときなんかに便利です。

;; "user_bank"という名前のテーブルは存在しない。
(defstruct user-bank
  user-id
  name
  bank-balance)

(retrieve-one
  (select (:user_id
           :name
           (:as (:sum (:amount))
                :bank_balance))
    (from :user)
    (left-join :bank_transactions :on (:= :user.id :bank_transactions.user_id))
    (where (:= :name "nitro_idiot")))
  :as 'user-bank)
;=> #S(USER-BANK :USER-ID 1 :NAME "nitro_idiot" :BANK-BALANCE 200000)

いずれの例でも、結果として返ってきた構造体オブジェクトにsetfで変更を加えてもO/Rマッパーのようにデータベースに更新処理が行えるわけではありません。あくまでデータベースから構造体への一方向のマッピングだけを行います。

モデル定義としての構造体

少しずつ複雑な例を紹介していきます。

上の例では単なるCommon Lispの構造体を使いました。

これだけで十分な方も多いかもしれませんが、dataflyでは少し変わった構造体を定義する機能もあります。

使い方は簡単です。defstructの代わりにdefmodelというマクロを使います。

(defmodel user
  id
  name
  email
  registered-at)

アノテーションライブラリのcl-annotを使うと@modelと書くこともできます。

(annot:enable-annot-syntax)

@model
(defstruct user
  id
  name
  email
  registered-at)

以下では@modelを使うものとします。

inflate

@modelをつけると構造体定義にいくつかの特殊なオプションをつけることができます。

その一つが:inflateです。

@model
(defstruct (user (:inflate registered-at #'datetime-to-timestamp))
  id
  name
  email
  registered-at)

(:inflate <カラム> <関数>)を記述すると、オブジェクトを作るときに指定した<カラム>の値に<関数>を自動適用します。この例ではregistered-atというカラムをLOCAL-TIMEのTIMESTAMPオブジェクトに変換します。

(defvar *user*
  (retrieve-one
    (select :*
      (from :user)
      (where (:= :name "nitro_idiot")))
    :as 'user))

;; Returns a local-time:timestamp.
(user-registered-at *user*)
;=> @2014-04-15T04:20:13.000000+09:00

:inflateは複数つけることもできます。また、<カラム>の部分をリストにして複数のカラムを指定することもできます。

オブジェクトからデータベースにINSERT/UPDATE/DELETE文を発行する機能は無いので、反対の:deflateはありません。

:has-a と :has-many

他に指定できるオプションとして:has-a:has-manyがあります。これらはテーブルのカラムの関係性を定義することで、構造体にアクセサを追加する機能です。

@model
(defstruct (user (:inflate registered-at #'datetime-to-timestamp)
                 (:has-a config (select :* (from :config) (where (:= :user_id id))))
                 (:has-many (tweets tweet)
                  (select :*
                    (from :tweet)
                    (where (:= :user_id id))
                    (order-by (:desc :created_at)))))
  id
  name
  email
  registered-at)

(defstruct config
  id
  user-id
  timezone
  country
  language)

(defstruct tweet
  id
  user-id
  body
  created-at)

この例ではuser-configuser-tweetsというアクセサが自動で定義されます。

(defvar *user*
  (retrieve-one
    (select :*
      (from :user)
      (where (:= :name "nitro_idiot")))
    :as 'user))

(user-config *user*)
;=> #S(CONFIG :ID 4 :USER-ID 1 :TIMEZONE "JST" :COUNTRY "jp" :LANGUAGE "ja")

(user-tweets *user*)
;=> (#S(TWEET :ID 2 :USER-ID 1 :BODY "Is it working?" :CREATED-AT @2014-04-16T11:02:31.000000+09:00)
;    #S(TWEET :ID 1 :USER-ID 1 :BODY "Hi." :CREATED-AT @2014-04-15T18:58:20.000000+09:00))

:has-a:has-manyで定義されたアクセサを呼び出すとSELECT文が発行されることに注意してください。

結果は初回でキャッシュされるので、二度以上呼び出しても何回もクエリが発行されるわけではないので安心してください。キャッシュを消すにはclear-object-cachesが使えます。

おわりに

Integralと違ってブログポスト一つでほとんどの機能が紹介できてしまった。JSON吐くだけのWeb APIサーバとかならこの程度で十分ですね。

今回作ったdataflyはGitHubで公開しています。

また、来週の火曜の夜は渋谷でLisp Meetupがあります。興味がある方はどうぞご参加ください。

Lisp Meet Up presented by Shibuya.lisp #16 : ATND
日時: 4/22(火) 19:30 〜 21:30
場所: 渋谷マークシティ ウエスト13階 セミナールーム

参考

株式会社はてなを退職しました

二月末日で株式会社はてなを退職しました。二年半の間、大変お世話になりました。

理由。はてなで働き続けて得られる以上のことをしようと思ったから。

この一年くらい、僕は今の自分に何の価値も感じられず、今の自分に何の満足もできていない。それなのに、気を抜いたら現状に甘えて、一年後の自分が想像できる範囲の成長しかできなくなってる。

年末に一年間を振り返るとき「驚くべき進歩だ」と思えなかったら、きっと努力が足りてないんです。そして、一年後の自分が予想できるなら、今歩いている道は間違ってるんだと思う。

そんなことを考えつつ、ちょうど携わったサービスも終了したということもあって、居心地の良いはてなと大好きな京都を離れることにしました。

特に今後について現状で言えることは何もないですし、振り返るほどの立派な功績もないので、よくある退職エントリみたいにかっこいい文章は書けないですが、ご報告として。

こちらからは以上です。

誰向けかわからないCommon Lispでの関数型プログラミング入門とその未来

Lispと言えば関数型言語という印象を持つ人が多いようです。

まあ正直に言うと、Common Lispに関して言えば違うんですけどね。Common Lispは効率のためと言えばループも代入も使いまくるし、構造体もクラスもある。実際書かれたコードも関数型プログラミングとは程遠いことも多くて、たとえば僕が作ったClackのコードを見ればオブジェクト指向言語だって言っても信じると思います。

僕自身、繰り返しをわざわざ再帰で書くよりもloop使うことが多いです。最近loopに頼りすぎてて良くないなーと思うことが多く、Common Lispでも性能が重要でないところは関数型っぽく書く癖をつけないとなー、と思っていろいろ考えています。なんでもloopだと可読性が悪い。

特に、僕が今作っているCommon Lisp方言の「CL21」では関数プログラミングをもっとしやすくする機能を入れたいと思っています。

そういうわけで、最近はCommon Lispでの関数型プログラミングの方法について調べてCL21に取り込むことをしています。

だいたいまとまったので、Common LispとCL21で関数型プログラミングをするチュートリアルみたいなものを午前四時のローテンションで書いてみました。

以下の2章立てです。

今読み返してみると、しれっとScheme知ってる前提だったりして、これ誰向けだよ、みたいな感じですが、まあご容赦ください。というかCL21のほうが本題だったりするので入門っぽい話を読みたくなかったら2章までスクロールしてください。

Common Lispでの関数型プログラミングの現状

関数

Common Lispで関数を定義するにはdefunを使います。

(defun 関数名 (パラメータ*)
  "ドキュメント文字列 (任意)"
  本体*)

例えば、Schemeで良く使われるiotaは以下のように定義できます。iotastartから始まるn個のリストを返します。

(defun iota (n &optional (start 0))
  (if (zerop n)
      nil
      (cons start (iota (- n 1) (1+ start)))))

(iota 10)
;=> (0 1 2 3 4 5 6 7 8 9)

(iota 5 3)
;=> (3 4 5 6 7)

無名関数

Common Lispで無名関数を作るにはlambdaを使います。

(lambda (n)
  (and (zerop n)
       (integerp n)))
;=> #<Anonymous Function #x302002ECCE3F>

無名関数を単に呼び出すときは、通常の関数の位置にlambdaフォームを書けばいいだけです。

((lambda (n)
   (and (zerop n)
        (integerp n)))
 3)
;=> NIL

((lambda (n)
   (and (zerop n)
        (integerp n)))
 0)
;=> T

((lambda (n)
   (and (zerop n)
        (integerp n)))
 0.0)
;=> NIL

高階関数

高階関数とは、関数を引数として受け取ったり、返り値として(無名)関数を返すような関数のことです。

例えば、多くの言語ではmapという関数がありますね。関数とリストを受け取り、リストのそれぞれの要素について関数を適用するようなものです。

Common Lispではこのような機能はmapcarと呼ばれています。

(mapcar #'1+ '(1 2 3 4 5))
;=> (2 3 4 5 6)

#'という記号は他の言語では特殊なので説明が必要ですね。

まずCommon Lispでは関数と変数の名前空間が分かれています (LISP-2という分類)。たとえば、Common Lispには関数listがありますが、同時に同じ名前のlistという変数を定義して使うこともできます。

このとき気をつけなければいけないのは、単にlistと書いたときに、それが変数なのか関数なのかを区別する必要があるということです。Common Lispで通常listを値として評価した場合、変数として扱われます。

(defvar list '(1 2 3))

list
;=> (1 2 3)

listを関数として扱いたいときは、#'をつけます。

#'list
;=> #<Compiled-function LIST #x3000000B4D6F>

上のmapcarでは1+という変数を渡しているのではなく、関数1+を渡したいので、#'が必要なのです。

また、reduceもよく使われる高階関数です。reduceはリストの先頭から渡された関数に適用し、さらにその返り値とリストの次の値を適用することを繰り返して結果を返す関数です。reduceは他の言語ではfoldとかinjectと呼ばれることもあります。

;; (+ (+ 1 2) 3) と同じ
(reduce #'+ '(1 2 3))
;=> 7

高階関数の使い道

上述した高階関数の便利なところは、その汎用性です。引数として渡す関数によってさまざまな用途に使えます。小さく汎用的なパーツを組み合わせて段々と大きくしていく手法はボトムアッププログラミングとして知られていますね。

たとえば、reduceを使うと以下のような関数が簡単に定義できます。

(defun sum (list)
  "数字のリストを受け取り、その合計値を返す"
  (reduce #'+ list))

(defun factorial (n)
  "数字を一つ受け取り、その階乗を返す。 n! = 1 * 2 * 3 * ... * n"
  (reduce #'* (iota n 1)))

sumは数字のリストを受け取り、その合計値を返します。factorialは数字を一つ受け取り、その階乗を返します。

mapcarではzipという関数が定義できます。

(defun zip (&rest lists)
  (apply #'mapcar #'list lists))

applyは一番最後の引数をリストとして扱って関数を呼び出す方法です。

zipは複数のリストを受け取り、その要素一つ一つをまとめあげる関数です。

(zip '(1 2 3) '(a b c) '(松 竹 梅))
;=> ((1 A 松) (2 B 竹) (3 C 梅))

関数合成 (compose)

さて、高階関数を使って組み合わせることで多くの関数を定義することを説明しました。この章ではさらに「関数合成」というテクニックを紹介します。

たとえば、関数gの返り値を関数fに渡したいとき、(f (g x)) のように書けばいいのはわかりますね。

この処理を高階関数に渡すには、無名関数を作るのが最もナイーブな方法です。

(lambda (x)
  (f (g x)))

しかし、組み合わせる関数がもっと多くなったときはどうでしょうか。

(lambda (x)
  (f (g (h (i (j (k x)))))))

長くなりますしだんだんと読みづらくなってしまいます。

このようなとき便利なのが「関数合成」です。ここでは関数composeを使います。関数composeCommon Lispの仕様に含まれないため、Alexandriaのようなユーティリティライブラリを使うか、もしくは以下のようにreduceで簡単に定義できます。

(defun compose (fn &rest functions)
  (reduce (lambda (f g)
            (lambda (&rest args)
              (funcall f (apply g args))))
          functions
          :initial-value fn))

composeを使うとさき先ほどの例は以下のようになります。

(compose #'f #'g)

関数を引数で渡すだけなので括弧も少なく済みますね。

例として、リストの各要素の sin(n + 1) をリストとして返す処理は以下のように書けます。

(mapcar (compose #'sin #'1+) '(1 2 3 4 5))
;=> (0.9092974 0.14112 -0.7568025 -0.9589243 -0.2794155)

mapcarを2回使っても同じ結果が出ますが、リストを2回走査する必要があるし、ループのたびにリストを新しく作るためメモリ消費面でも良いコードではありません。

;; 良くない
(mapcar #'sin
        (mapcar #'1+ '(1 2 3 4 5)))

conjoin & disjoin

関数合成の例として、他にもconjoindisjoinという関数もよく使われます。

これらは各関数の返り値の真偽によって関数を複数実行する機能です。

たとえば、「ゼロかつ整数である」という条件の処理は以下のように書けます。

(lambda (n)
  (and (zerop n)
       (integerp n)))

この処理もcomposeのときのように、適用する関数が多い場合に煩雑になってしまいます。

このような処理を関数合成で解決するのがconjoinです。

(import 'alexandria:conjoin)

(funcall (conjoin #'zerop #'integerp) 0)
;=> T

(funcall (conjoin #'zerop #'integerp) 0.0)
;=> NIL

conjoinandで関数を繋げた無名関数を返します。一方でdisjoinorで関数を繋げます。

たとえば(disjoin #'plusp #'minusp)はプラスかマイナスの数値なら真を返します。つまりゼロではないという条件になります。

(funcall (disjoin #'plusp #'minusp) 100)
;=> T

(funcall (disjoin #'plusp #'minusp) 0)
;=> NIL

ちなみに余談ですが、ゼロかどうかはzeropで判断できるので、その返り値を反転させて返すほうが賢い実装ですね。

(funcall (lambda (n) (not (zerop n))) 0)
;=> NIL

このようなnotを加えるだけの呼び出しもよく使われるので、単に返り値の真偽を反転させる関数を返す関数complementというものもあります。

(funcall (complement #'zerop) 0)
;=> NIL

CL21での関数型プログラミングの未来

さて、ここからが実は本題です。

Common Lisp関数型プログラミングの機能をひと通り紹介しました。Common Lispでも十分に関数型プログラミングができますね。

しかし、僕が作っている新しいCommon Lisp方言の「CL21」では、より関数型プログラミングをしやすいようにするつもりです。たとえば、Common Lispには無いcomposeconjoindisjoincurryrcurryを含めました。

それだけでなく、さらにそれを簡易に使えるリーダマクロもあります。#'です。

Common Lisp#'はシンボルか、lambda式にしか使えなかったのですが、CL21ではこのリーダマクロに機能を追加したもので上書きしています。

実際のコードを見せたほうが早いかもしれません。

(funcall (conjoin #'zerop #'integerp) 0)
;=> T

;; ↑と同じ
(funcall #'(and zerop integerp) 0)
;=> T


(funcall (disjoin #'plusp #'minusp) 0)
;=> NIL

;; ↑と同じ
(funcall #'(or plusp minusp) 0)
;=> NIL

#'(and zerop integerp) と書くと conjoin に展開され、#'(or zerop integerp) と書くと disjoin に展開されます。

これは単に短いだけでなく、andorという一般的な単語を使うことで直感的です。

さらに、もう想像つくと思いますが、complementに対応するものはnotです。

(funcall (complement #'zerop) 0)
;=> NIL

;; ↑と同じ
(funcall #'(not zerop) 0)
;=> NIL

もちろん、これらは組み合わせて使うこともできます。

(remove-if-not #'(and integerp
                      (or (not evenp)
                          zerop))
               (iota 11))
;=> (0 1 3 5 7 9)

composeだけはそのままcomposeを使います。

(mapcar (compose #'sin #'1+) '(1 2 3 4 5))
;=> (0.9092974 0.14112 -0.7568025 -0.9589243 -0.2794155)

;; ↑と同じ
(mapcar #'(compose sin 1+) '(1 2 3 4 5))
;=> (0.9092974 0.14112 -0.7568025 -0.9589243 -0.2794155)

あまりルールを増やすのは良くないとは思いますが、こういう地味に省略されててかつ見た目もわかりやすいという機能はどんどん取り入れていきたいですね。

ちなみにCL21はGitHubで絶賛開発中で、意見募集や議論はIssuesで行っています。興味があればぜひご参加ください。

まとめ

JavaScriptで学ぶ関数型プログラミング

JavaScriptで学ぶ関数型プログラミング

Lisp Meet Up #13 に参加しました

1/23の夜開催されたイベント、Lisp Meet Up presented by Shibuya.lisp #13 に参加しました。

毎月やっているLisp Meet Upが1周年を迎えたのはめでたいですね。なわたさんと神田さんは表彰されていいと思う。

参加者

最初に自己紹介タイムがありました。使っているLispClojureが一番多かったです。Common Lisp回なのにCommon Lisperは一番少なかったんじゃないかな。

Integralの紹介をしました

先日作ったCommon LispのO/RマッパーのIntegral について発表しました。30分くらい話したと思います。

既存のCLのO/Rマッパー「Postmodern」と比較し、より開発フローを意識していろいろ良い感じにやってくれるんだよ、という話です。

先日のブログエントリでは書かなかった、どのように実装されているか、という話を中心に話をしようと思っていたので、CLOSとMeta Object Protocolの話も多かったです。Clojure使いの多い会でやるべき話ではなかった……。

後で佐野さんに、「『classをmake-instanceする直前にslotを…』みたいにずっと横文字しゃべってた」って茶化されましたが、Common Lisperには評判が良かったので良しとします。

κeenさんのCIMの発表

前日にタイトルが明らかになったκeenさんの発表は、「Common Lisp Implementation Managerを作りました」という、自分の作ったツールの紹介でした。

CIMの読み方は「ちむ」だそうです。かわいいですね。

このツールは簡単に説明するとCommon Lisp for RVMで、CL処理系とそのバージョンを複数インストールして切り替えることができるもの。タイトル聞いたときから、もうこれが僕が欲しかったものだ!って感じです。

ライブラリを作っていると複数の処理系で試すのが結構面倒で、Jenkinsで自動テストを回すにしてもそれぞれの処理系のコマンドライン引数の差と苦闘しないといけない。SBCLは複数バージョンを同じ環境にインストールするのが面倒で、毎回手動で切り分けをやったりしていました。

この辺りをShellyでいくらか改善しようとしたんだけど、CIMのほうがぴったり需要に合っています。実装もShellyみたいにPerlが必要ではなく、Bourne Shellで書かれているのも良い。

まだバグも多いようですが、αリリース版を試したい方は以下のリポジトリのREADMEに使い方が書いてあります。

Common Lisp処理系について

休憩時間、ayato_pさんに「Common Lisp処理系は何がいいですか」と聞かれました。

隣にいた佐野さんはSBCLを使っておけばいいんじゃないか、と言っていましたが、僕はClozure CLを推しました。

Common Lisp処理系は何がいいか、と聞く人は初心者だから、初心者が困らない処理系をおすすめしなければならない」と思っていて、SBCLはその点ではあまり親切ではありません。エラーメッセージがわかりづらかったり、ASDFでライブラリロードしたときに、Style Warning程度でコンパイルエラーになったりする。そのときはデバッガが立ち上がるから良い、というのはCommon Lispを既に書いている人の意見で、他の言語から来た人はデバッガが立ち上がると混乱するし怖いと思います。そういう理由で、聞かれたら大体Clozure CLをおすすめしています。

SBCLは簡単な型チェックをしてくれるし、何より高速なので、慣れてきたら移るか併用するのがいいかなと思います。

Rubyを追いかける

ayato_pさんがブログで書かれていますが、今回の発表の2つともにRubyの話が出てきました。僕のORMの発表ではActiveRecordが、κeenさんの発表ではRVMが比較として出されました。

言語の表現力や実行速度は確実に優っているのに、その人気度は遥かに引き離されているのは、Common Lispを普及したいと思っている人の悩みの種です。Lispを始めたいと思った人でも、最近はClojureに行ってしまう。今回の参加者のCommon Lisperの数を見ても、Common Lispは全く人気のある言語とは言えない。

ここからはMeet Upと関係無いし完全に私見ですが……もうカッコつけてる場合じゃないと思うんですよ。なりふり構わずRubyClojureを追いかけないといけない。

特にClojureにはヒントがあると考えていて、なぜCommon LispではなくClojureを始める人が多いのか、採用する企業が日本でも増え始めたのかということを真剣に考えないといけない。Clojureを始める人がなぜLispの中でもClojureを選ぶのか。JVMに載ってるからという安心感もあると思いますが、モダンなLispだから、という理由も多くあると僕は推測しています。

先日CL21という方言を紹介しました

CL21の評価は賛否両論でしたが、普段Common Lispを書かない人たちには「良い意味で当たり障りのない言語に見える」などそれなりに評判が良かったのは面白い反応でした。Common Lispも見せ方を変えるだけで反応は変わるのだな、と思いました。

一方でCommon Lisper達には「Rubyみたいな遅い言語になっちまうだろーが」とか「自分はこういうレイヤーは必要ないと思う」などボロクソに言われました。

この反応の差に、Common Lispを普及したいと思っている人は向き合っていかないといけないと思います。

おわりに

話はイベントに戻る……。

今回は2つとも発表がPracticalだったね、と佐野さんが言っていました。良いことです。この調子でもっとLispを盛り上げていきたいですね。

こちらのイベントレポートもご覧ください。

新しい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みたいな遅い言語になっちまうだろーが」ってコメントがついていて面白いですね。