t-cool

Common Lispで小さなプロジェクトを始めるには?


本記事は、原著者の許諾のもと、翻訳・掲載しています。 Starting a minimal Common Lisp project / Phil Eaton


Common Lispで小さなプロジェクトを始めるには?

2018年3月5日, Phil Eaton

もし以前にLispについて少し話を聞いたり、学校でSchemeを習ったことがあったとしても、Common Lispは、あなたが想像するものとは全く違います。Schemeは、関数型プログラミングで人気ですが、Common Lispは、理論的に純粋なプログラミング言語というよりも、実世界での利用を強く意識して設計されたプログラミング言語です。さらに、人気がある処理系のSBCLは、高度に最適化されたコンパイラであり、Javaに対抗できるものです。

部品を組み立てる

Common Lispのシンボルは、第一級の変数(ラベル)であり、パッケージと呼ばれる名前空間に収められます。しかしパッケージは、ディレクトリを超える範囲は対処しません。ディレクトリを超えてソフトウェアを構成するには、ASDFの「システム」を使います。Common LispのパッケージはPythonのモジュール、ASDFのシステムはPythonのパッケージのようなものです。ASDFは、ローカル環境にない依存関係を管理しません。その用途にはQuicklispを使います。Quicklispは、事実上、Common Lispのパッケージ管理システムです。ASDFは、Common Lispの処理系に付属していることが多く、SBCLにも付属しています。Quicklispは、処理系に付属しません。

Quicklispを入手する

導入方法はQuicklispのサイトでも説明されていますが、基本的な手順は次の通りです:

$ curl -O https://beta.quicklisp.org/quicklisp.lisp
$ sbcl --load quicklisp.lisp
...
* (quicklisp-quickstart:install)
...
* ^D
$ sbcl --load "~/quicklisp/setup.lisp"
...
* (ql:add-to-init-file)

小さなパッケージ

これでプロジェクトを始める準備ができました。あなたが作りたいライブラリの名前で、ディレクトリ(フォルダ)を作ってください。例えばDockerのラッパーライブラリを作るとして、"cl-docker"というディレクトリを作りましょう。そのディレクトリの中には、".asd"の拡張子をつけて、同じ名前でファイルを作ります:

$ mkdir ~/projects
$ cd ~/projects
$ mkdir cl-docker
$ touch cl-docker/cl-docker.asd

ASDFは、ディレクトリの中で".asd"ファイルを探すので、".asd"ファイルは、ディレクトリと同じ名前をもつことが重要です。パッケージ化をする前に、まずはライブラリからエクスポートする関数を書きます。名前は重要ではありませんが、ここでは"cl-docker/docker.lisp"という名前でファイルを作り、以下のように編集してください:

(defun ps ()
  (let ((output (uiop:run-program '("docker" "ps") :output :string)))
    (loop for line in (rest (cl-ppcre:split "(\\n+)" output))
      collect (cl-ppcre:split "(\\s\\s+)" line))))

ここでは、uiopライブラリが使われています。uiopはASDFに組み込まれているので、明示的に読み込む必要はありません。uiopは、サブプロセスとして"docker ps"コマンドを実行して、出力を文字列として返します。その後、cl-ppcreライブラリから正規表現のsplit関数を使い、出力の先頭(output first)を行(lines)に分割します。最初の行以外の全てを取り込み、複数の行を、1個以上の空白文字(空白・タブ・改行文字)に基づいて分割します。

次に、Pythonでのモジュールに相当する「パッケージ」を定義しましょう。"cl-docker/package.lisp"のファイルを編集します。

(defpackage cl-docker
  (:use cl)
  (:import-from :cl-ppcre :split)
  (:export :ps))

ここでは、cl-dockerという名前でパッケージを定義しています。(:use cl)はCommon Lispのシンボルをパッケージにインポートすること、(:import-from :cl-ppcre :split) はcl-ppcreパッケージからsplitシンボルをインポートすること、(:export :ps)はps関数のみをエクスポートすることを意味します。

ここでは、"cl-docker/docker.lisp"の中に、そのファイルが、cl-dockerパッケージの一部であることを宣言する必要があります:

(in-package :cl-docker)

(defun ps ()
  (let ((output (uiop:run-program '("docker" "ps") :output :string)))
    (loop for line in (rest (cl-ppcre:split "(\\n+)" output))
      collect (cl-ppcre:split "(\\s\\s+)" line))))

次に、"cl-docker/cl-docker.asd"に、システムを定義しましょう。Common Lispのシステムは、Pythonのパッケージにあたります:

(defsystem :cl-docker
    :depends-on (:cl-ppcre)
    :serial t
    :components ((:file "package")
                 (:file "docker")))

これでASDFのシステムで必要なものを定義することができます: システムの名前、パッケージの定義、パッケージのコンポーネント("cl-docker/docker.lisp")を定義して、ASDFには"cl-ppcre"システムがディスク上で利用できるようにしておく必要があります。

また、ASDFにはコンポーネントが、書いた順番通りに実行されるようにする必要があります。(補足 :componentsの前に:serial tと書くことで、順番通りにコンポーネントが読み込まれます)

cl-ppcreシステム(依存関係があるシステム)がディスク上にない場合に備えて、Quicklispを通して間接的にシステムを読み込むことができます。Quicklispを使うと、依存関係にあって不足しているシステムをオンラインから取ってきます。

その前に、もしこのディレクトリが"~/common-lisp"にある場合を除いて、システムを読み込む際に、ASDFとQuicklispがどこを検索するかを知らせるために、自身のシステム定義があるディレクトリを登録する必要があります。このためには、"~/.config/common-lisp/source-registry.conf.d/"以下に、".conf"ファイルを次のように追加する必要があります。

(:tree "~/システムファイルがあるディレクトリ")

もし"cl-docker"というレポジトリが、"~/projects"ディレクトリに"cl-docker"ディレクトリとしてある場合、"~/.config/common-lisp/source-registry.conf.d/1-cl-docker.conf"を作って、次のように書きます:

(:tree "~/projects/cl-docker")

システムを使う

ここまで終えると、あなたのコンピュータのどこからでも、自作のライブラリを使うことができます。Common LispのREPLを開き、Quicklispでシステムを読み込みましょう。ローカルにない依存関係にあるシステムも読み込まれます:

$ sbcl
...
* (ql:quickload "cl-docker")
To load "cl-docker":
  Load 1 ASDF system:
    cl-docker
; Loading "cl-docker"
..................................................
[package cl-docker]
("cl-docker")
* (cl-docker:ps)

これで終わりです!

例のソースコードは、このGistを確認してください。

最後に

Common Lispは、使いやすくてパッケージも多くあり、成熟しています。ASDFでパッケージを設定するのは、Pythonで"setup.py"を設定するよりもシンプルです。

ASDFで依存関係にあるバージョンを固定する方法を説明しませんでしたが、それもできます。(参考: Qlotを参照ください。) ここで取り上げた方法はシンプルですが、Quicklispの作者のZach Beaneが開発しているquickprojectを使うと、雛形を作ってくれます。(参考: Fukamachiさんのcl-projectも同様の機能を提供しているライブラリです)

Common Lispの情報源

Practical Common Lispは、オンラインで無料で読むことができます。(参考: 邦訳は実践Common Lispとして出版されています) Practical Common Lispは、最高の情報源の1つであり、問題に直面したときに参考にし続けています。

Paul Graham氏のOn Lispも、Lispのマクロを理解しようとするときに必読の書籍です。 Schemeでマクロを書くときにも、役にたつでしょう。この本は絶版になっていますが、私は、LuluにPDFを送って、送料込みで20ドル以下でコピーを手に入れることができました。(参考: 邦訳はこのリンクから入手可能です。)

私は現在、Common Lisp the Language, 2nd Editionを読み進めています。こちらもオンラインで自由に閲覧できます。これはCommon Lispを実装しようと考えている人以外には、あまりおすすめできませんが、悪くないアイデアだと思います。

最後に、Peter Norvig氏のParadigms of Artificial Intelligence Programming (邦訳題: 実用Common Lisp - AIプログラミングのケーススタディ)がちょうどオンラインで無料で閲覧できるようになりました。私はまだ読んでいませんが、読むのを楽しみにしています。タイトルを見て恐れないでください。この書籍はCommon lispの実用的な案内書であり、伝統的なAIを取り上げています。

Schemeについて

私は、Chicken SchemeでWebプロトタイプをいくつか作ってみました、Chicken Schemeは、R5RS/R7RS準拠のScheme処理系です。Chicken Schemeは、Web開発で最善策とは思いません。ネイティブスレッドの対応もありませんし、(nginx等に)組み込みが容易にできる軽量のインタプリターがあるだけです。Chicken Schemeがニッチなのは、サードパーティーの良質なライブラリをもつ高品質の処理系であるからですが、Scheme処理系としては最速のSchemeとはいえません。

Scheme R6RSから派生したRacketを用いて、より大きなWebプロトタイプに取り組みました。Github issueを報告してくれるアプリを制作しました。Racketについては、ブログに気に入ったことを記事に書きました。

Racketは、JITコンパイラをもつ高性能なインタプリタで、スレッドのサポートもあり、サードパーティーのライブラリのコレクションも充実しています。Racketのエコシステムは、Haskelのと同じ問題があります。つまり、ライブラリと束縛(bindings)は、主に概念を証明するためだけであり、ドキュメントやテスト、実用性に欠けます。 FlaskでのJinjaのように、S式を用いてテンプレート化されたHTMLファイルをレンダリングしようとすると、悪夢を見ることになります。

Racketには申し訳ないですが...

最後に、Racketでのデバッグは楽しくありませんでした。バックトレースは、全く役に立ちませんでした。単純に考えると、この機能は、Racketが関数を最適化したり書き直したりするために使われると思います。 エラーを見つけたり修正したりすることができずに、Racketをうまく使うことができませんでした。

一方でCommon Lispは...

Common Lispは、処理系やエコシステムのおかげで堅固であり、開発も活発です。特にSBCLは、素晴らしいパフォーマンスとネイティブサポートがあり、バックエンドの開発において、有望な候補の一つです。