Roswell で環境構築して Common Lisp を書いてみる

なんか Lisp ってよく聞くよなーと思って、ちょっと触ってみる。

目次

Common Lisp の概要をお勉強する

Lisp は1958年に登場したプログラミング言語。マクロ機能により構文そのものを拡張できるなど柔軟性があり、様々な方言が存在する。

2022年現在で広く使われている方言は Common Lisp。その他に Clojure、Scheme などが有名なようだ。今回は Common Lisp を扱ってみる。

C 言語におけるコンパイラが gcc だけではないのと同じように、Common Lisp にも処理系がいくつかある。中でも有名な実装は Steel Bank Common Lisp (SBCL) や CLISP など。

パッケージマネージャもあり、Quicklisp が有名。他には Another System Definition Facility (ASDF) というモノもあるみたい。

調べていくと、Roswell というツールが処理系のインストールからパッケージマネージャの役割までフルスタックで担ってくれて Windows 対応もしているそうなので、一番使いやすそうかと思い、今回環境構築に利用する。

Roswell をインストールして環境構築する

Mac の場合、Homebrew からインストールできる。Linux の場合も同じ Linuxbrew でインストールできる。

$ brew install roswell

動作確認してみようと思い $ ros コマンドを叩いてみたら、いきなりデフォルトの処理系などがインストールされ始めた。

$ ros
Installing sbcl-bin...
No SBCL version specified. Downloading sbcl-bin_uri.tsv to see the available versions...
[##########################################################################]100%
Installing sbcl-bin/2.2.8...
Downloading https://github.com/roswell/sbcl_bin/releases/download/2.2.8/sbcl-2.2.8-x86-64-darwin-binary.tar.bz2
[##########################################################################]100%
Extracting sbcl-bin-2.2.8-x86-64-darwin.tar.bz2 to /Users/Neo/.roswell/src/sbcl-2.2.8-x86-64-darwin/
Building sbcl-bin/2.2.8... Done.
Install Script for sbcl-bin...
Installing Quicklisp... Done 14223
Making core for Roswell...
Common Lisp environment setup Utility.

Usage:

   ros [options] Command [arguments...]
or
   ros [options] [[--] script-path arguments...]

commands:
   run       Run repl
   install   Install a given implementation or a system for roswell environment
   update    Update installed systems.
   build     Make executable from script.
   use       Change default implementation.
   init      Creates a new ros script, optionally based on a template.
   fmt       Indent lisp source.
   list      List Information
   template  Manage templates
   delete    Delete installed implementations
   config    Get and set options
   version   Show the roswell version information

Use "ros help [command]" for more information about a command.

Additional help topics:

   options

Use "ros help [topic]" for more information about the topic.

Roswell を経由してインストールした処理系やライブラリなどは、全て ~/.roswell/ 配下に配置されるようなので、アンインストールしたくなったら ~/.roswell/ をまるっと削除すれば良い。

$ ros version
roswell 21.10.14.111(NO-GIT-REVISION)
build with Apple clang version 13.0.0 (clang-1300.0.29.3)
libcurl=7.77.0
Quicklisp=2021-02-13
Dist=2022-07-08
lispdir='/usr/local/Cellar/roswell/21.10.14.111/etc/roswell/'
homedir='/Users/Neo/.roswell/'
sbcl-bin-variant=''
$ ros config
setup.time=3872220259
sbcl-bin.version=2.2.8
default.lisp=sbcl-bin

Possible subcommands:
set
show
$ ros install
Usage:

To install a new Lisp implementaion:
   ros install impl [options]
or a system from the GitHub:
   ros install fukamachi/prove/v2.0.0 [repository... ]
or an asdf system from quicklisp:
   ros install quicklisp-system [system... ]
or a local script:
   ros install ./some/path/to/script.ros [path... ]
or a local system:
   ros install ./some/path/to/system.asd [path... ]

For more details on impl specific options, type:
   ros help install impl

Candidates impls for installation are:
abcl-bin
allegro
ccl-bin
clasp-bin
clasp
clisp
cmu-bin
ecl
mkcl
sbcl-bin
sbcl-head
sbcl
sbcl-source

# 以下でも同様
$ ros list versions

デフォルトでインストールされた処理系は SBCL v2.2.8 だった。

$ ros run -- --version
SBCL 2.2.8

# 以下でリスト表示
$ ros list installed
Installed implementations:

Installed versions of sbcl-bin:
sbcl-bin/2.2.8

$ ros install で処理系をインストールして、$ ros use でグローバルに使用する処理系を変更する、という感じみたい。rbenv や nvm なんかのバージョン管理ツールを使ったことがあれば分かると思う。

ライブラリのインストールも $ ros install で出来るようで、内部的には前述の Quicklisp が使われている模様。

Roswell で SBCL の REPL を試す

$ ros run と打つと、デフォルトに指定した処理系の REPL が立ち上がる。ココで動作確認してみる。

$ ros run

# Hello World
* (print "Hello World")

"Hello World" 
"Hello World"

# 算術
* (+ 1 2)
3

# 終了する場合 : Ctrl + D で強制終了も覚えておくと良いかも
* (exit)

Common Lisp プロジェクトの雛形を作ってみる

cl-project というモノを使うと、プロジェクトの雛形 (スケルトン) が作れるらしい。

$ ros run で REPL を起動し、その後の2行を打つ。

$ ros run
* (ql:quickload :cl-project)
* (cl-project:make-project #P"hello-project" :author "Neos21")

# 終わったら (exit) で REPL を抜ける
* (exit)

# こんな風にファイルが出力されている
$ tree -a ./hello-project
./hello-project
├── .gitignore
├── README.markdown
├── README.org
├── hello-project.asd
├── src
│   └── main.lisp
└── tests
    └── main.lisp
2 directories, 6 files

src/main.lisp というファイルを編集していけば良いようだ。

エントリポイントとなる Roswell ファイルを作る

$ ros init コマンドを使うと、エントリポイントとなる実行可能ファイルを作れるらしい。

$ cd ./hello-project/
$ ros init hello-project
Successfully generated: hello-project.ros

中身は以下のようになっている。シェルスクリプトとして起動するが、自身を ros コマンドに渡して Common Lisp のコードを実行させるようだ。

#!/bin/sh
#|-*- mode:lisp -*-|#
#|
exec ros -Q -- $0 "$@"
|#
(progn ;;init forms
  (ros:ensure-asdf)
  #+quicklisp(ql:quickload '() :silent t)
  )

(defpackage :ros.script.hello-project.3872814791
  (:use :cl))
(in-package :ros.script.hello-project.3872814791)

(defun main (&rest argv)
  (declare (ignorable argv))
  ; ココにメイン処理を書く
)
;;; vim: set ft=lisp lisp:

; ココにメイン処理を書く と書いた部分に、

(write-line "Hello World!")
(print "Hello World!")

こんな感じのエコー文を書いてみる。そして次のように実行する。

$ ros ./hello-project/hello-project.ros
# もしくは直接実行しても大丈夫
$ ./hello-project/hello-project.ros

とりあえずエコーはできたが、まだ src/main.lisp を全く使えていない。

エントリポイントから src/main.lisp を呼び出す

src/main.lisp を書き換えていって、「パッケージ」を作る。ココでは hello-project というプロジェクト名で、echo-hello-world() という関数をエクスポートしている。

(defpackage hello-project
  (:use :cl)
  (:export :echo-hello-world)  ; 関数をエクスポートする
)
(in-package :hello-project)

(defun echo-hello-world ()
  (write-line "hello-project/src/main.lisp !")
)

続いてエントリポイントとなる hello-project.ros ファイルを書き換えて、この hello-project パッケージの echo-hello-world() 関数を呼び出すようにする。

#!/bin/sh
#|-*- mode:lisp -*-|#
#|
exec ros -Q -- $0 "$@"
|#
(progn ;;init forms
  (ros:ensure-asdf)
  #+quicklisp(ql:quickload '(:hello-project) :silent t)
  ; ↑元は `'()` だった部分でパッケージをインポートしている
  )

(defpackage :ros.script.hello-project.3872814791
  (:use :cl))
(in-package :ros.script.hello-project.3872814791)

(defun main (&rest argv)
  (declare (ignorable argv))
  ; ココにメイン処理を書く
  
  ; ↓ ココで関数を呼び出している
  (hello-project:echo-hello-world)
  
  0  ; 終了ステータス
)
;;; vim: set ft=lisp lisp:

さて、コレで hello-project.ros を実行すれば、src/main.lisp が読み込めるはず…と思ったら、どうやってもパッケージが「Not Found」といったエラーが出てしまう。何だこれ…。

プロジェクトは Roswell のディレクトリ配下に置かないといけない (重要!)

色々調べてみると、自作パッケージを解決するには、そのプロジェクトが ~/.roswell/local-projects/ 配下に配置されていないといけないようだ。

先程の .ros ファイルを見てもらうと、quicklisp の機能を使って自作の hello-project パッケージをインポートしようとしている。この Quicklisp を Roswell が上手く隠蔽してくれている都合もあり、自作のプロジェクトを ~/.roswell/local-projects/ 配下に置いておかないといけないようである。素の Quicklisp がよく分かっていないが、少なくとも Roswell による環境構築で一緒にインストールされる Quicklisp を利用する場合は、こういうことらしい。

Node.js 民が想像しやすい例えをするなら、Quicklisp は「npm のグローバルインストールしたディレクトリ」しか探索できない、という感じだろうか。この辺、モダンな言語だともう少し上手くやってくれるけど、言語やエコシステムの歴史が長い Common Lisp の都合があり、それを Roswell が頑張ってパッケージングしてくれているため、Roswell Way を理解して使わないといけない感じだ。

実際のプロジェクトディレクトリを ~/.roswell/local-projects/ 配下に移動させて利用しても良いが、シンボリックリンクを張るという方法でも良い。

# Quicklisp でパッケージが認識できるようにシンボリックリンクを張る
$ ln -s "$(pwd)" "${HOME}/.roswell/local-projects/$(basename "$(pwd)")"

# コレで上手く動作するようになる
$ ros ./hello-project/hello-project.ros

コレを解決するのに時間がかかった…。

ビルドするには

こうしてとりあえず作れた Common Lisp プロジェクトだが、最後にビルド方法を。

エントリポイントとなる .ros ファイルを指定して $ ros build を実行すれば、単一のバイナリファイルが出来上がる。このシングルバイナリを持ち運べば、Roswell や Common Lisp の処理系がなくとも動作する。

$ ros build ./hello-project/hello-project.ros
# ↓以下のようにバイナリファイルができる
$ ./hello-project/hello-project

ついでに:シングルファイルでの実行

ついでに発見したこと。.lisp ファイルに main 関数さえ書いてあれば、$ ros ./single-file.lisp のように直接実行できる。ただし、$ ros build はできなかった。

(defun main ()
  (print "single-file.lisp !")
)

ひとまず以上

Roswell によって、SBCL 処理系や Quicklisp・ASDF といったパッケージ管理ツール等をまるっとインストールしてもらい、Common Lisp プロジェクトの雛形作成・ビルドなどをしてもらえるようになった。

ココから先は Common Lisp の構文の勉強もそうだし、途中で ~/.roswell/local-projects/ 配下にシンボリックリンクを張ったように、Roswell がラップしている Quicklisp や ASDF などの仕組みも理解しておかないといけないだろう。

とりあずドックフーディングの初歩ということで、今回はココまで。