CL入門 No.4 - REPL

REPL?

REPLとはRead-Eval-Print Loopの略称で、
ソースコードを一行読み、評価(実行)し、評価結果を出力するループ処理のことです。

ほとんどのCL処理系が実行するとREPLが立ち上がります。
OSにおけるシェルのように、ユーザとCommon Lisp処理系が対話するための、
基本的な仕組みです。

REPLを作る

最もシンプルなREPLは次で作成できます。

(loop (print (eval (read))))

loop, print, eval, readはいずれもCommon Lisp標準の構文と関数なので、
このREPLはあらゆるCommon Lisp環境で動作するでしょう。*1

まずは上のコードを自分のCommon Lisp処理系で動作させてみましょう。
Common Lispの処理系が一体何者なのか、その一端に触れたような気がしませんか?

Slime-Listener

もちろん処理系標準のREPLで開発することもできますが、
もう少し多機能なREPLを使ってみましょう。

Slime標準のREPLはいくつかのコマンドが与えられており、
REPLで開発していくのに便利です。

Package名表示

REPLが立ち上がると、

CL-USER>

と表示されていませんか?
CL-USERとは現在のカレントパッケージ*2の名前です。
パッケージはCommon Lispの重要なシステムですが、同時に非常に難解なシステム*3でもあるので、また後日まとめていく予定です。

Common Lispではパッケージを移動しながらプログラムが実行されていくので、
シェルのディレクトリ表示のように、
ユーザがどこにいるか分かることは作業を進める上で役に立ちます。

入力候補

コードの入力途中でTABキーを押すと、候補リストが表示されます。
入力途中で名前が分からなくなったときに便利です。
もちろん入力補完することもできます。

ラムダリスト表示

構文や関数のシンボルの後はラムダリスト*4emacsの下部に表示されています。
入力しているパートには強調表示がつくので、書いている途中で自分がどの部分を書いているか見失わないようにできます。

複数行入力

C-jでインデントを整えながら次の行を書くことができます。
REPLで複雑なコードを書くことはあまりないと思いますが、
入れ子が深くなったら複数行にまたいだほうが見やすいでしょう。

C-aで現在入力しているコードの先頭にカーソルを戻すことができます。

C-c M-o または M-x slime-repl-clear-buffer でREPLのログをクリアすることができます。
ログが目障りになってきたら、キレイにしましょう。

REPLで開発してみる

REPLでちょっとしたプログラムを開発してみましょう。
今回は、ライフゲームを作成してみます。

ライフゲームを知らない人は、
まずこちらを読んでみてください。
wikipedia:ライフゲーム

これから多くのCommon Lisp機能を使っていきます。
それぞれの機能に関しては、後の記事で紹介していきます。

1. 盤面を用意する

まずは盤面を用意してみましょう。

(defparameter *board* (make-array (* 32 16) :initial-element nil))

2. 表示機能を作る

標準のprintで*board*を確認しても状態が分かりにくいので、
フォーマット出力*5を作成します。

(defun format-board (list)
  (format t "~%~{~<~%~,32:;~a~>~}" list))

(defun collect-render-list ()
  (loop for x across *board* collect (if x #\X #\_ )))

(defun render ()
  (format-board (collect-render-list)))

関数を細かく分けることで、後々にプログラムを修正しやすくなります。*6
プロトタイプ開発の場合では重宝する手段です。

3. 盤面操作機能を作る

盤面操作用にいくつかの機能を作成します。

(defun location (x y) (+ x (* y 32)))

(defun birth (x y)
  (setf (aref *board* (location x y)) t))

(defun kill (x y)
  (setf (aref *board* (location x y)) nil))

(defun exist? (x y)
  (and
    (and (>= x 0) (< x 32) (>= y 0) (< y 16))
    (aref *board* (location x y))))

繰り返し使う式は関数化させます。

本当なら盤面外アクセスの例外処理*7を含めるところですが、
今回はコードを簡潔にするために省略しています。

ただし、八近傍のチェックがあるので、
exist?だけは端処理が必須になります。

4. ライフゲームのルールを作る

ライフゲームのルールを作成します。

(defun count-neigbor (x y)
  (loop for bx from -1 to 1 sum
    (loop for by from -1 to 1 count
      (and (not (and (= bx 0) (= by 0)))
           (exist? (+ x bx) (+ y by))))))

(defun update-cell (x y)
  (let ((c (count-neigbor x y)))
    (cond
      ((= c 3) (birth x y))
      ((<= c 1) (kill x y))
      ((>= c 4) (kill x y)))))

(defun update-board ()
  (loop for x below 32 do
    (loop for y below 16 do
      (update-cell x y))))

打ち込んでみたら分かると思いますが、
REPLではこれくらい打つだけで疲れてきますね。
しかもタイプミスしたときに修正が面倒です。

プログラムが複雑になってくると、
REPLのみでの開発は難しいことに気が付くと思います。

5. 更新機能を作る

最後に、盤面を更新して現在の状態を出力させます。

(defun life-game-step (&optional (count 1))
  (loop repeat count do (update-board))
  (render))

ライフゲームは真っ白な盤面だと何も起きないので、
適当にbirthしてからlige-game-stepしましょう。

(birth 10 9)
(birth 10 8)
(birth 10 7)
(birth 11 8)
(birth 13 9)
(render)
(life-game-step)

まとめ

REPLでの開発はCommon Lispではごく自然です。

次はREPLと連携して使用するであろう
DebugとInspectについてまとめていきます。

*1:最も、振舞いは処理系依存になると思いますが。

*2:CL::*Package*というシンボルに束縛されているパッケージオブジェクトのこと。

*3:Common Lispのパッケージ機能は、他の言語処理系が裏で処理しているシンボル解決機能が、むき出しになっているようなものです。

*4:構文や関数のボディや引数の意味表記法。仕様書で厳密に書き方が決まっている。

*5:Common Lispにはformatという強力な書式付出力機能があります。

*6:無駄に関数名を使ってしまうので、本番プログラムでは綺麗に整形する必要があります。

*7:普通ならConditionを使用します。