🚀

ECLでCommon Lispに入門する

に公開

はじめに

Common Lispをはじめて触ったとき、REPLを使って対話的にコードを評価させ、Hello,Worldが動いた瞬間に「完全にマスターした!」と思ったのは私だけなのか、そうではないのか。それはさておき、その先の道が険しいと気付くのに、それほど多くの時間は必要なかった。

他の言語であれば──例えばCであれば、ライブラリの読み込み先を指定するのにC言語の構文など使わないし、コンパイルするコマンドでC言語のコードを記述する必要もなかったことを思い出します。それと比較し、Lispでは、Lispへの理解を前提とした世界が広がっています。

EmacsとSlime(Sly)による開発は、Emacsの設定ファイル(Lisp)を書かなければならないし、ビルドシステムのASDFは、Common Lispのライブラリとして実行時に読み込まれるので、ASDFの設定をしたい場合にもLispのコードを理解しておかなければなりません。
ライブラリの依存関係を理解し、自動的にライブラリを取得、インストールしてくれるQuicklispもまた実行時に読み込んで使うため、その設定はLispのコードを書く必要に迫られます。
そして最後に、Common Lisp本体の設定もまた、Lispのコードを書く必要があります。

公開されているライブラリを活用して何か簡単なプログラムを書いてみようという地点に辿り着くまでに、Lispへの理解を前提とした環境が初心者の前に立ちはだかります。

  • プロジェクトのディレクトリを作成し
  • ASDFの作法に則った構造でディレクトリ、ファイルを作成し
  • そのプロジェクトのために使用するライブラリをインストールし
  • EmacsとSlyで実装を行い
  • テストし
  • そしてビルドする
  • Gitでコードを管理し
  • 他人に共有した際、同じようにビルドできるようにしておく

と言った普通のことをしたいと考えたとき、それらを実現するには、Common Lispを取り巻く様々な情報を集め、理解しておく必要があります。特定の便利なツールを使えば、ある程度これらの煩わしい部分を隠蔽、解決してくれるものの、その中身を知りたいと思ったのも事実です。

ということで、Hello,Worldの先に向かおうとするときに割と面倒な、ディレクトリ単位でのプロジェクト管理、ASDFを使ったシステム定義、実行からビルドまでを一旦まとめようと思います。

尚、周囲にはLisperもLispフレンズも、Lispエイリアンもいないぼっちなので、そんなことしちゃだめだよってところがあれば指摘をもらえると嬉しいです。

ECL

$ pacman -S ecl

などで気軽に入れることができます。eclと打てばREPLが起動します。ここではそれ以上のことは気にしなくて大丈夫です。

ASDFについて

ASDF (https://asdf.common-lisp.dev/) は以下のようにasdf:defsystemを使い記述した<project-name>.asdファイルを用意し、適切に依存するライブラリを読み込んだ上でCommon Lispの処理系にロードするために使用します。他にも、コードのコンパイルに使うこともあります。

(asdf:defsystem "myproject"
  :description "A sample Common Lisp project"
  :version "0.1.0"
  :author "Your Name"
  :license "MIT"
  :depends-on (:cl-ppcre :alexandria)
  :serial t
  :components ((:file "package")
               (:file "myproject")))

自分のプロジェクトにたくさんの*.lispファイルが存在しているだけでも、その全てをいちいち手動でCommon Lisp上にロードするのは面倒なものです。ASDFを使わなくともコードは書けますが、プロジェクトとしてある程度の規模のコードを書く場合には、*.asdファイルを用意したほうが快適です。

ここでひとつ問題があります。ASDFが探すライブラリの場所は、標準では~/common-lispと言ったホームディレクトリ直下の場所であったり、ライブラリをコンパイルした際のキャッシュもまた、~/.cache/common-lispという形で、ホームディレクトリ直下の.cacheに作られるようになっています。

C言語であれば、/usr/libにライブラリパスを通すことに何の疑問もありませんが、他の多くの言語はプロジェクト単位で依存するライブラリを保持するようになっていることが多いように思えます。
任意の場所、できればプロジェクトを作成したディレクトリ以下に依存するライブラリを設置し、読み込むようにしたいものです。

ASDFはCommon Lispのライブラリのため、実行時にASDFの関数を呼び出すことでASDF自体の設定を変更することができます。これはCommon Lispの利点ですが、設定を行うために(まだ不慣れな)Lispコードを書かなければならないというデメリットもあります。

ASDFでは、initialize-source-registryという関数で読み込むライブラリのディレクトリを指定し、initialize-output-translationsという関数でライブラリのコンパイル済みファイルの出力先を指定することができます。

具体的なコードは、以下のようになります。

(require :asdf)
(defvar *current-dir* (namestring (uiop:getcwd)))
(asdf:initialize-source-registry
 `(:source-registry
   (:tree ,(concatenate 'string *current-dir* "/.deps/systems"))
   (:tree ,(concatenate 'string *current-dir* "/"))
   :ignore-inherited-configuration))
(asdf:initialize-output-translations
 `(:output-translations
   (t ,(concatenate 'string *current-dir* "/.deps/cache"))
   :enable-user-cache
   :ignore-inherited-configuration))

(require :asdf)で、ASDFを使えるようにしています。
次に*current-dir*というグローバル変数に、(namestring (uiop:getcwd))という式の結果を渡しています。(uiop:getcwd)はASDFにバンドルされているユーティリティライブラリで、現在の作業ディレクトリのパスオブジェクトを取得します。namestringは、パスオブジェクトを文字列に変換します。

こうして現在のディレクトリを取得できた後、asdf:initialize-source-registryでライブラリ読み込み先を設定します。

(asdf:initialize-source-registry
 `(:source-registry
   (:tree ,(concatenate 'string *current-dir* "/.deps/systems"))
   (:tree ,(concatenate 'string *current-dir* "/"))
   :ignore-inherited-configuration))

この設定では、現在のディレクトリと、現在のディレクトリ直下の.deps/systemsというディレクトリからライブラリを読み込むように設定しています。また、ignore-inherited-configurationによって、デフォルトの読み込み先を無効にしています。

以下のasdf:initialize-output-translationsの設定は、/.deps/cacheにコンパイル済みファイルを出力するよう設定しています。

(asdf:initialize-output-translations
 `(:output-translations
   (t ,(concatenate 'string *current-dir* "/.deps/cache"))
   :enable-user-cache
   :ignore-inherited-configuration))

C言語で言えばMakefileのようなものかもしれませんが、C言語の外にMakefileというツールがあることに対して、Common LispではASDFというツール(ライブラリ)をCommon Lisp自体が解釈・実行する点が大きく異なる部分だと思います。

ECLでASDFを読み込む

ASDFの設定のためのコードを、実行の度に記述するのは面倒です。Common Lispでは実行時に事前に評価するコードを、初期化ファイルとして設定します。ECLの場合、標準で~/.eclrcという設定ファイルを読み込み起動します。.eclrcの中身は、Common Lispのコードを記述できるので、前述のASDFの設定をそのまま記述しておけば、起動時にライブラリの読み込み先、コンパイル済みファイルの出力先の設定が有効になります。

ということで、~/.eclrcの中身は、先ほど提示したASDFのコードを記述しておきます。

(require :asdf)
(defvar *current-dir* (namestring (uiop:getcwd)))
(asdf:initialize-source-registry
 `(:source-registry
   (:tree ,(concatenate 'string *current-dir* "/.deps/systems"))
   (:tree ,(concatenate 'string *current-dir* "/"))
   :ignore-inherited-configuration))
(asdf:initialize-output-translations
 `(:output-translations
   (t ,(concatenate 'string *current-dir* "/.deps/cache"))
   :enable-user-cache
   :ignore-inherited-configuration))

ECLを起動すれば、ASDFによるライブラリの読み込み先、コンパイルしたファイルの出力先は既に設定されている状態になります。

Quicklisp

ASDFは依存するライブラリを読み込み、ビルドするためのシステムですが、肝心のライブラリ自体を取得する機能はありません。大事なことなのでもう一度言いますが、依存するライブラリをインターネットから取得しインストールする機能はありません。そのため実際にライブラリを取得する場合はQuicklisp (https://www.quicklisp.org/beta/) という別のツールを使用します。

Quicklispは、Quicklispに登録されているライブラリ(ASDFで定義されたシステム)を検索し、手元にダウンロード、インストールし、更にCommon Lispに読み込む機能も備えています。しかしこのツールも標準で使う場合、~/quicklispにライブラリがインストールされてしまいます。

できるだけプロジェクト毎にファイルやライブラリを管理したい方針のため、プロジェクトディレクトリにライブラリがインストールされるようにしたいところです。

そこで、以下のように適当なプロジェクトディレクトリを作成し、Quicklispをインストールするためのlispコードをダウンロードします。

$ mkdir myproject
$ cd myproject
$ mkdir -p .deps/{systems,cache}
$ wget https://beta.quicklisp.org/quicklisp.lisp

quicklisp.lispを取得した後、ECLのREPLを起動し、

> (load "quicklisp.lisp")
> (quicklisp-quickstart:install :path ".deps/systems")

という形で、:pathにインストール先のディレクトリを指定しインストールします。以下のようなメッセージを確認できれば、Quicklispのインストールは完了です。

  ==== quicklisp installed ====

    To load a system, use: (ql:quickload "system-name")

    To find systems, use: (ql:system-apropos "term")

    To load Quicklisp every time you start Lisp, use: (ql:add-to-init-file)

    For more information, see http://www.quicklisp.org/beta/
NIL
>
> (quit)

でECLのREPLを終了させ、Quicklispのインストール先がどうなったかを確認してみます。treeコマンドを使い、.depsの中の構成を確認してみましょう。

$ tree .deps
.deps
├── cache
│   └── home
│       └── hiro
│           └── Projects
│               └── myproject
└── systems
    ├── asdf.lisp
    ├── client-info.sexp
    ├── dists
    │   └── quicklisp
    │       ├── distinfo.txt
    │       ├── enabled.txt
    │       ├── preference.txt
    │       ├── releases.txt
    │       └── systems.txt
    ├── local-projects
    ├── quicklisp
    │   ├── bundle.lisp
    │   ├── bundle-template.lisp
    │   ├── cdb.lisp
    │   ├── client-info.lisp
    │   ├── client.lisp
    │   ├── client-update.lisp
    │   ├── config.lisp
    │   ├── deflate.lisp
    │   ├── dist.lisp
    │   ├── dist-update.lisp
    │   ├── fetch-gzipped.lisp
    │   ├── http.lisp
    │   ├── impl.lisp
    │   ├── impl-util.lisp
    │   ├── local-projects.lisp
    │   ├── minitar.lisp
    │   ├── misc.lisp
    │   ├── network.lisp
    │   ├── package.lisp
    │   ├── progress.lisp
    │   ├── quicklisp.asd
    │   ├── setup.lisp
    │   ├── utils.lisp
    │   └── version.txt
    ├── setup.lisp
    └── tmp
        ├── install-dist-distinfo.txt
        └── quicklisp.tar

12 directories, 34 files

まだ何もライブラリを入れていないため、Quicklisp関連のファイルのみ確認することができます。~/.eclrcにQuicklisp関連のロードの設定を記述することで、ECL起動時にQuicklispを自動的にロードするようにもできますが、Quicklispの役割を、依存ライブラリのダウンロードとインストールに限定したいため、記述しないことにします。

ASDFとQuicklispで依存ライブラリをダウンロード・インストールする

先ほど作成したmyprojectディレクトリ直下に、myproject.asdというファイルを以下の内容で作成します。

(defsystem "myproject"
           :serial t
           :pathname "src"
           :components ((:file "package")
                        (:file "main"))
           :depends-on ("alexandria" "cl-ppcre"))

:pathname "src"は、:componentsで読み込むファイルのディレクトリパスを指定します。:componentsでは、このプロジェクトで読み込むLispファイルを記述します。:serial tは、:componentsで読み込むファイルの順序を、記述どおりの順序で読むよう指定します。最後に、:depends-onは、依存しているライブラリを記述します。

src/package.lispと、src/main.lispというふたつのファイルを用意します。src/main.lispは、依存ライブラリのalexandria:curryを使ったコードにします。

;; src/package.lisp
(defpackage :myproject
  (:use :cl)
  (:export :main))
;; src/main.lisp
(in-package myproject)

(defun main()
  (defparameter *add1* (alexandria:curry #'+ 1))

  (format t "加算結果: ~a~%" (funcall *add1* 5))

  (let ((numbers '(10 20 30)))
    (format t "元のリスト: ~a~%" numbers)
    (format t "加算後のリスト: ~a~%" (mapcar *add1* numbers))))

ファイルを作成したら、myprojectディレクトリ以下のファイル、ディレクトリ構造が以下のようになっているかを確認します。

$ tree ./
./
├── myproject.asd
├── quicklisp.lisp
└── src
    ├── main.lisp
    └── package.lisp

ECL REPL上でQuicklispを使い、まずはこのmyprojectが依存しているライブラリを取得、インストールします。
最初に、Quicklispのsetup.lispをロードし、REPL内でQuicklispの関数を実行できるようにします。

> (load ".deps/systems/setup.lisp")

#P"/home/hiro/Projects/myproject/.deps/systems/setup.lisp"

これでQuicklispが使えるようになったので、ql:quickloadを使って、myproject自身をロードすることで依存ライブラリのダウンロード、インストールを行います。asdファイルに記述された依存ライブラリが自動的インストールされる様子は、Nodeを使う際のpackage.jsonに似ているかもしれません。

>  (ql:quickload "myproject")
To load "myproject":
  Load 1 ASDF system:
    myproject
; Loading "myproject"
To load "alexandria":
  Load 1 ASDF system:
    asdf
  Install 1 Quicklisp release:
    alexandria
; Fetching #<url "http://beta.quicklisp.org/archive/alexandria/2024-10-12/alexandria-20241012-git.tgz">
; 55.94KB
==================================================
57,281 bytes in 0.05 seconds (1181.68KB/sec)
; Loading "alexandria"
[package alexandria]..............................
[package alexandria-2].
To load "myproject":
  Load 1 ASDF system:
    myproject
; Loading "myproject"
To load "cl-ppcre":
  Load 1 ASDF system:
    asdf
  Install 1 Quicklisp release:
    cl-ppcre
; Fetching #<url "http://beta.quicklisp.org/archive/cl-ppcre/2025-06-22/cl-ppcre-20250622-git.tgz">
; 153.92KB
==================================================
157,615 bytes in 0.15 seconds (1053.23KB/sec)
; Loading "cl-ppcre"
[package cl-ppcre]................................
..........
To load "myproject":
  Load 1 ASDF system:
    myproject
; Loading "myproject"
[package myproject]
("myproject")
> 

asdファイルに記述していた、alexandria、cl-ppcreがダウンロード、インストールされたことがわかります。これらのライブラリは、.deps/systems以下にインストールされます。

ASDFだけでプロジェクト依存ファイルは読み込める

一旦REPLを終了させ、再びREPLを立ち上げます。依存関係はASDFが解決するため、実行やビルドはQuicklispを使わずに行うことができます。Quicklispをロードせず、ASDFでロードした上で、myprojectのmain関数を呼び出してみます。

> (asdf:load-system "myproject")

T
> (myproject:main)
加算結果: 6
元のリスト: (10 20 30)
加算後のリスト: (11 21 31)

ASDFでビルド

問題なく実行できることが確認できたら、このプログラムを実行可能な形でビルドします。:epilogue-codeで、プログラムの実行開始の関数を呼び出し、それが完了すれば(quit)を実行するという処理を記述しています。

> (asdf:make-build :myproject
                   :type :program
                   :move-here #P"./"
                   :monolithic t
                   :epilogue-code '(progn (myproject:main) (quit)))

(#P"/home/hiro/Projects/myproject/myproject")

myprojectディレクトリに、myprojectという実行ファイルが作成されていればビルド成功です。

$ ls -la
total 3536
drwxr-xr-x  4 hiro hiro    4096 Jul  1 22:21 .
drwxr-xr-x 10 hiro hiro    4096 Jul  1 22:09 ..
drwxr-xr-x  4 hiro hiro    4096 Jul  1 21:33 .deps
-rwxr-xr-x  1 hiro hiro 3542112 Jul  1 22:21 myproject
-rw-r--r--  1 hiro hiro     203 Jul  1 21:50 myproject.asd
-rw-r--r--  1 hiro hiro   57144 Jan 29  2015 quicklisp.lisp
drwxr-xr-x  2 hiro hiro    4096 Jul  1 22:08 src

$ ldd myproject
	linux-vdso.so.1 (0x00007c19f7ac9000)
	libecl.so.24.5 => /usr/lib/libecl.so.24.5 (0x00007c19f7400000)
	libc.so.6 => /usr/lib/libc.so.6 (0x00007c19f7210000)
	libgmp.so.10 => /usr/lib/libgmp.so.10 (0x00007c19f7917000)
	libffi.so.8 => /usr/lib/libffi.so.8 (0x00007c19f790b000)
	libm.so.6 => /usr/lib/libm.so.6 (0x00007c19f7118000)
	libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007c19f78dc000)
	/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007c19f7acb000)

$ ./myproject
加算結果: 6
元のリスト: (10 20 30)
加算後のリスト: (11 21 31)

参考

Discussion