Common Lisp 入門
(SBCL)

関連記事

1. SBCLをインストールする

Common Lispでプログラミングをはじめてみたいと思います。

Common Lisp は、1984年に登場した言語で、関数を書いてすぐに動かせる対話的な開発環境が特徴です。
コードを書いて、即座に結果を見て、また書く。
この繰り返しにプログラミングの楽しさがあると思います。

まずは、Common Lisp の処理系をインストールして、関数を書いて動かしてみましょう。
ここでは、無料で広く使われている Steel Bank Common Lisp(SBCL)を使います。

1.1. Windows

https://www.sbcl.org/platform-table.html からインストーラをダウンロードして実行します。

1.2. macOS

Homebrew を使ってインストールします。

brew install sbcl

1.3. Ubuntu / Debian

sudo apt install sbcl

2. REPLを起動する

インストールしたら、ターミナルまたはコマンドプロンプトで sbcl と打ちます。

$ sbcl
This is SBCL 2.4.0
*

* がプロンプトです。
ここに式を入力して Enter を押すと、すぐ結果が返ってきます。

* (+ 1 2)
3

これが REPL で、コード読み取ると、評価して結果を表示します。
REPL の繰り返しを終了するには (quit) と打ちます。

2.1. 関数呼び出しの形を知る

Common Lisp のコードは、すべて括弧で囲まれた形をしています。
最初は少し面食らうものですが、規則はシンプルです。

たとえば Python や JavaScript では、関数をこう呼び出します。

print("hello")
max(a, b)
equal(a, b)Code language: PHP (php)

Common Lisp では、こう書きます。

(print "hello")
(max a b)
(equal a b)Code language: PHP (php)

カンマがなくなり、開き括弧が関数名の前に移動しただけです。
「関数名が先、引数が後」という順序は同じです。

ちなみに、+* のような演算子も、Common Lisp では普通の関数として同じ形で書きます。

(+ 1 2)      ; => 3
(* 3 4)      ; => 12
(+ 1 2 3 4)  ; => 10Code language: PHP (php)

演算子も関数なので、3個以上の数もをまとめて計算できます。
; から行末はコメントで、ここでは => で返ってくる値を示しています。

複雑な計算は括弧を入れ子にして、内側から計算します。

(+ (* 2 3) (* 4 5))  ; => 26Code language: PHP (php)

内側の (* 2 3) が先に評価されて6になり、(* 4 5) が20になり、最後に足されて26が返ります。
すべての計算には括弧があるのでちょっと複雑に見えますが、演算子ごとの特別な優先順位や結合規則はなく、関数と同じように扱われます。

2.2. 関数を定義する

複雑な計算は、関数を定義すると何度も使えます。

たとえば、1からnまでの整数の和を求めてみます。
これは、 n(n+1)/2 という公式で計算できます。
REPL に直接 defun を打って、関数を定義してみましょう。

* (defun sum-to (n)
    (/ (* n (+ n 1)) 2))
SUM-TO

defun は、関数名、引数リスト、本体を定義します。
sum-to が関数名、
(n) が引数リストで、
本体の (/ (* n (+ n 1)) 2) は n×(n+1)÷2 を計算します。

定義が成功すると、REPLは定義した関数名 SUM-TO を表示します。

定義した関数は、REPLで引数を変えるだけで何度でも呼び出せます。

* (sum-to 10)
55

* (sum-to 100)
5050

同じ計算式を繰り返し書かないで済むのが、関数のメリットです。

2.3. エラーが出たら

REPL でエラーが出ると、デバッガに入ります。

* (sum-to)
debugger invoked on a SB-INT:SIMPLE-PROGRAM-ERROR:
  invalid number of arguments: 0Code language: JavaScript (javascript)

怖くありません。
q を打てばトップレベルに戻ります。

0] q
*

エラーメッセージを読んで、コードを直して、また load する。
それだけです。

3. コードをファイルに残す

ただし、REPL に入力した内容は、SBCL を終了すると消えてしまいます。

書いたコードを残しておくには、メモ帳などのテキストエディタにコピーして、デスクトップなどに保存しておきます。
たとえば、 mylisp.lisp というファイルを作り、さきほど打ったコードを貼り付けて保存します。

(defun sum-to (n)
  (/ (* n (+ n 1)) 2))Code language: Lisp (lisp)

3.1. ファイルをREPLに読み込む

SBCL は、load でファイルを読み込んで関数が復元できます。

* (load "/Users/ユーザー名/Desktop/mylisp.lisp")Code language: JavaScript (javascript)

ファイルの保存場所まで指定するのでちょっと面倒ですが、Windows のコマンドプロンプトでは (load " まで打ったあと、エクスプローラーからファイルをウィンドウにドラッグするとパスが自動入力されます。
macOS のターミナルでは、” で囲んでから同様にドラッグでパスして入力できます。

3.2. 関数を育てる

関数を修正するときは、テキストエディタで書き直して、また load するようにすればコードを残しておけます。

たとえば、sum-to を使って、aからbまでの整数の和を求める関数 sum-range を書き足してみましょう。

これは、1からbまでの和から、1からa-1までの和を引けば求められます。

(defun sum-to (n)
  (/ (* n (+ n 1)) 2))

(defun sum-range (a b)
  (- (sum-to b) (sum-to (- a 1))))Code language: Lisp (lisp)

ファイルを保存して、SBCLのREPLで load し直します。
それから、呼び出してみます。

* (load "/Users/ユーザー名/Desktop/mylisp.lisp")
T
* (sum-range 4 10)
49

* (sum-range 11 20)
155Code language: JavaScript (javascript)

sum-range の中で sum-to を呼んでいます。
一度定義した関数は、別の関数の部品として使えます。

3.2. 関数を育てる

書き直すたびに load して結果を確かめる。
この繰り返しが Common Lisp でのコードの育て方の基本です。

3.3. ループで計算する

sum-to は n(n+1)/2 という公式を使って一発で答えを出しました。
公式がわからなくても、loopを使えば同じ計算ができます。

試しに、1からnまでの整数の和をループで書いてみます。

* (defun sum-to-loop (n)
    (loop for i from 1 to n
          sum i))
SUM-TO-LOOPCode language: JavaScript (javascript)

loop for i from 1 to n は「iを1からnまで1ずつ増やす」という意味です。
sum i は「iを累積して合計する」という指示です。

* (sum-to-loop 10)
55

* (sum-to-loop 100)
5050

sum-to と同じ結果が返ってきました。

3.4. 二乗の和

1からnまでの整数の二乗の和を求めてみましょう。
1² + 2² + 3² + … + n² です。

実は、二乗の和は n(n+1)(2n+1)/6 で計算できますが、覚えていなければループで解けます。

(defun sum-of-squares (n)
  (loop for i from 1 to n
        sum (* i i)))Code language: Lisp (lisp)

sum (* i i) は「iの二乗を累積する」という指示です。

* (sum-of-squares 5)
55

* (sum-of-squares 10)
385

直接計算する公式を知らなくても、プログラムを使うと複雑な計算結果を求められます。

4. リストの和を求める

ループを使うと、数値のリストを受け取って、その和を返す関数も書けます。

(defun sum-list (nums)
  (loop for n in nums
        sum n))Code language: Lisp (lisp)

loop for n in nums は「numsの各要素をnとして取り出す」という意味です。
sum n でnを累積し、ループが終わると合計値が返ります。

* (sum-list '(1 2 3 4 5))
15

* (sum-list '(10 20 30))
60Code language: PHP (php)

'(1 2 3 4 5) はリストのリテラルです。
クォート ' は「評価せずにそのままデータとして扱う」という印です。

4.1. 標準入力から数を受け取る

さらに、ターミナルから数を入力して結果を得られるようにしてみることもできます。
1行に1つ数を入力し、空行で終了したら合計を表示する関数です。

(defun sum-from-input ()
  (let ((nums (loop for line = (read-line *standard-input* nil nil)
                    while (and line (string/= line ""))
                    collect (parse-integer line))))
    (princ (sum-list nums))
    (terpri)))Code language: Lisp (lisp)

これを使うと入力待ちになり、空行を入力するまで繰り返します。

* (sum-from-input)
10
20
30

60Code language: JavaScript (javascript)

3つの数を入力して空行を打つと、合計の60が返ってきました。

read-line は1行読み込みます。
第2・第3引数の nil nil は、ファイル末尾やエラーのときに nil を返すという指定です。
string/= は「文字列が等しくない」という比較で、空行でないあいだ続けます。
parse-integer は文字列を整数に変換し、princ で値を出力、terpri で改行します。