Clozure CL Part3

EmacsとSLIMEの設定のおさらい。Common Lispにおける定数、スペシャル変数(グローバル変数)、レキシカル変数(局所変数)についての説明。

EmacsとSLIME

Clozure CL Part2ではプログラミングそのものの説明と、簡単な計算式の書き方について説明した。Part3以降では、計算式と計算式を組み合わせたり、値を保持する方法など、より複雑なプログラムを記述する方法について説明していく。

その前に、Clozure CL Part1で設定した開発環境のおさらいをする。今後、REPLに直接入力する方法とあわせて、ファイルにプログラムコードを記述し、その内容をREPLに転送することでプログラムの実行を確認していくためだ。

以下の画像は、Emacsを起動し、左側にSLIME、右側にsample.lispという何も書かれていないテキストファイルを開いた状態になっている。

EmacsとSLIMEを起動した状態

この状態にするための手順を説明する。Emacsの操作の詳細に関しては、Emacsマニュアルの翻訳がある。Emacsの操作説明で出てくるC-xはCtrlとxキーを押すことを示し、M-xはAltキーとxキーを押すことを示す。

まずは、プログラミングを行うためのディレクトリを作成しよう。 Terminalを起動し以下を入力することで、Projectsディレクトリとその下のcommon-lispというディレクトリを作成し、その下に移動する。

$ mkdir -p ~/Projects/common-lisp/
$ cd ~/Projects/common-lisp/

次に、Emacsを起動する。

$ emacs &

Emacsをアクティブにした状態で、M-xを入力すると、画面左下にM-xという表示が出てくる。またその後ろに、カーソルが点滅状態になっていることが確認できるはずだ。

M-xを入力したEmacs

ここで、slimeと続けて入力してENTERキーを押すと、SLIMEが起動する。

M-x slimeと入力したEmacs

無事SLIMEが起動した場合は、以下のような表示になる。

SLIME起動直後のEmacs

Emacsが最初に開いているバッファが上にあり、SLIMEによって開かれたClozure CLのREPL環境のバッファが下にある状態だ。

Emacsのウインドウ分割操作で、ひとまずREPLのバッファのみ表示させる。C-x oで、今いるバッファをREPLのほうにする。C-x oは、入力するたびにアクティブなバッファを切り替えるためのコマンドだ。

REPLを選択した状態で、C-x 1を入力すると、REPLバッファのみになる。もし間違ったバッファを選択してしまっていたら、C-x bでバッファの一覧が確認できるので、その中から、*slime-repl ccl*というバッファを選択してENTERキーを押せば、REPLのバッファがアクティブになる。

次に、画面を左右に分割するために、C-x 3を入力する。ちなみにC-x 2を入力すると上下に分割されるため、もし左右に違和感があればC-x 2で上下に分割しても良い。

C-x 3で左右に分割すると、左右のバッファがともにREPLの表示になる。このうちの片方のバッファで、sample.lispというテキストファイルを開くために、C-x oで左右どちらかのバッファに移動する。

ファイルを新しく名前を付けて開くには、C-x fを入力する。すると、

Find file ~/Projects/common-lisp/

という表示がEmacsウインドウ下部に表示される。ここで、sample.lispというテキストを入力してENTERキーを押すと、sample.lispというファイル名で新しいバッファが作成される。Emacsウインドウ下部に(new file)と表示されれば成功だ。

このままではファイルはまだ保存されていない。C-x C-sと入力することで、sample.lisp(今はまだ何も書かれていないが)を保存できる。Emacsウインドウ下部に、Wrote /home/username/Projects/common-lisp/sample.lispと表示されれば、書き込まれたことになる。

SLIMEとLispプログラムコード用のファイルが用意できたところで、ファイルに記述したコードをREPL側でどのように認識させるかについて説明する。SLIMEのドキュメントには詳しい使い方が書いてあるが、差し当たって覚えておくコマンドは2つだ。

まず、使っていてよくわからなくなってしまったときにSLIMEを再起動するためのコマンドとして、

M-x slime-restart-inferior-lisp

を覚えておく。これを入力することで、SLIMEは起動直後の状態に戻る。

次のコマンドは、sample.lisp側のバッファを選択した状態で使用する。Lispの式の最後で、C-x C-e(Ctrl + cのあと、Ctrl + e)を入力すると、その式をREPL上で評価する。S式を評価したい場合は、閉じ括弧の次の位置にカーソルがあることを確認してから、C-x C-eを入力する。

値に名前を付ける

プログラムコードの中では、値に対して名前を付けることができる。他と区別し、人間にわかりやすくし、その名前を使って何度も利用するためだ。

定数の定義

今までの例では、計算やリストの操作を取り扱ったが、全て1回の実行で結果がわかるものだった。

CL-USER> (+ 1 2)
3

この3という得られた値を、別の処理、別の場所で使いたい場合はどうすれば良いだろうか。

実際に使いそうな数値を考えてみる。円周率3.14や重力加速度9.8、うまい棒は10円というように、世の中には大切な不変の数値というものがある。このように変更されることのない数値に対しては、定数の定義という仕組みを使う。

Lispでは、piをREPLに入力すると円周率の値が返ってくる。

CL-USER> pi
3.141592653589793D0

これは、Common Lispの処理系によって予めpiという名前に数値が紐付けられているためだ。しかし、重力加速度(g)は紐付けられていない。

CL-USER> g
# gという名前に何も値が紐付けられていないため、エラーになる

piと同じように、gで9.8が返ってくるようにしよう。sample.lisp側のバッファで、以下のプログラムコードを入力する。

(defconstant g 9.8)

このS式の閉じ括弧の次にカーソルを移動させ、C-x C-eと入力すると、このS式は評価され、REPL上でgという名前が利用できるようになる。

CL-USER> g
9.8

これは、gという名前に対して、9.8という値が紐付けられたことを意味する。 defconstantは、定数を定義するオペレーターであり、定数とは、一度定義すれば変更のできない値になる。定義された名前は、アトムのうちシンボルに属する。

(defconstant 定数名 値)

重力加速度を定義したので、10秒間で物体が落下する距離を求める計算を行ってみよう。落下距離をh(メートル)、経過する時間をt(秒)とすると、

\[ h = {\frac 1 2} {\times} g {\times} t^2 \]

となるので、これをCommon Lispの式にそのまましてしまう。

C-x C-edefconstantの式を評価したため、gはREPL上でそのまま使用できる状態だ。 tの2乗は、(expt 10 2)という式を使う。exptは、乗数を求めるオペレーターだ。 そして、分数の2分の1という値は、1/2という形でそのまま記述することができる。これらを踏まえると、以下のような式になる。

CL-USER> (* 1/2 g (expt 10 2))
490.0

無事490mという数値が出た。もし合っているかどうか不安であれば、手で計算して確認しよう。

このように値を意味のある名前にしておくと、名前を指定することで様々な計算で使いまわすことができる。また、defconstantは定数の定義であり、一度定義した名前に紐づく値が変化しないことが保証される。プログラムコード中で誤って値が上書きされることがないため、変化しない使いまわす値は、defconstantで定数として定義しておくと良い。

スペシャル変数(グローバル変数)

変数には、スペシャル変数(グローバル変数)と呼ばれるものと、レキシカル変数(局所変数)と呼ばれるものの2種類が存在する。スペシャル変数は、プログラムのどこからでも参照可能な変数で、レキシカル変数は、プログラム内の一定の箇所でのみ参照可能な変数だ。

この2つの使い分けはとりあえず脇に置いておいて、まずはスペシャル変数から説明する。

スペシャル変数の定義

スペシャル変数を定義する方法として、defvardefparameterという2つのオペレーターが存在する。defconstantと同じように、

(defvar 変数名 値)
(defparameter 変数名 値)

というような記述で定義を行う。例えば、先程の10秒という落下時間を変数名で保持しておくことを考えると、以下のように定義できる。

(defvar fall-time-1 10)

もしくは、以下のようにも書ける。fall-time-1と区別するため、fall-time-2という変数名にする。

(defparameter fall-time-2 10)

これをC-x C-eで評価し、REPL上で値を確認する。

CL-USER> fall-time-1
10
CL-USER> fall-time-2
10

defvarとdefparameterはまったく同じものではない。 defvarは、何度変数定義を実行したとしても最初に設定した値を保持し続けるのに対し、defparameterは、最後に実行した際の値を保持する。

この挙動を理解するためのコードを確認してみよう。まずは、10という値と紐付けられているfall-time-1に対して、REPL上で以下の処理を実行する。

CL-USER> (defvar fall-time-1 20)
FALL-TIME-1
CL-USER> fall-time-1
10
CL-USER> (defvar fall-time-1 30)
FALL-TIME-1
CL-USER> fall-time-1
10

念のため2回defvarを実行しているが、fall-time-1の値は10のままだ。つまり、最初に設定した10という値が保持され続けていることになる。

defparameterを使った場合の挙動を見てみよう。同じく10が紐付けられているfall-time-2に対して、20、30という値を設定する。

CL-USER> (defparameter fall-time-2 20)
FALL-TIME-2
CL-USER> fall-time-2
20
CL-USER> (defparameter fall-time-2 30)
FALL-TIME-2
CL-USER> fall-time-2
30

実行の度に値が再設定されていることがわかる。 また、defvarとdefparameterのもうひとつの違いは、定義時に値を必須とするかどうかという点だ。

(defvar a)

上記の式は、エラーにはならない。値が定まっていないが、変数用にaというシンボルが定義される。

(defparameter b)

しかし、上記のようにdefparameterを使った場合はエラーとなる。defparameterの場合は、第一引数である変数名に加えて、第二引数の値が必須となる。

この2つの方法の使い分けは、式が再評価された際、値がどのようになってほしいかという部分が深く関わってくるものの、一旦はこの2種類の定義方法があるということを覚えておく。

値の指定方法としては、以下のような書き方もできる。

CL-USER> (defvar v (+ 1 2 3))
V
CL-USER> v
6

上記のコードは、値として(+ 1 2 3)という式を指定している。計算の結果が、値として変数名と紐付く。

CL-USER> (defvar message "I love lisp")
MESSAGE
CL-USER> message
"I love lisp"

上記のコードは、文字列を値として指定している。messageには指定した文字列が紐付く。

スペシャル変数の値を変更する

変数は、値を変更できる。defparameterでは定義を行う際に値が変化したが、defvarでは値がこのままでは変更できない。どちらの方式で変数を定義したとしても、値を変更するためには通常、setqsetfというオペレーターを使用する。

10という値が設定されているfall-time-1の値を変更するためには、以下のようにする。

CL-USER> (setq fall-time-1 20)
20

setfも同様に使用できる。

CL-USER> (setf fall-time-1 30)
30

この2つの違いについては、別のパートで詳しく説明する。現段階では、どちらも変数に値を代入する同じような挙動を持つということで理解しておく。

レキシカル変数(局所変数)

どこからでも参照できるスペシャル変数に対して、レキシカル変数はある一定の範囲内で使用できる変数であり、letというオペレーターを使用する。

以下のコードを入力すると、formatオペレーターにより、”hello, world”という文字の出力が行われる。

CL-USER> (let ((message1 "hello") (message2 "world")) (format t "~A, ~A" message1 message2))
"hello, world"
NIL

ここで少し脇道にそれるが、formatオペレーターの第一引数がtであることに着目する。tはシンボルで、真偽値のtrue(真)であることを意味する。以前設定したnilというシンボルはfalse(偽)であることを意味する。formatオペレーターは、この第一引数の値を見て、文字列の出力先を決める。

  • 第一引数にnilを指定していた場合は、式の結果として値(文字)を返す。これは、(+ 1 2 3)の結果の6という値が返ってきていた状態と同じことを意味する。
  • 第一引数にtを指定した場合は、文字を標準出力に表示する。標準出力というと聞き慣れないかもしれないが、ここではREPLやTerminalへの出力と捉えれば良い。

今までnilを指定していても文字が表示されていたのは、REPLが返ってきた値=結果を出力するという挙動をしているためだ。もしREPL上ではなく、今後のパートで説明する単独のプログラムとして実行している場合には、明示的に出力先を指定しなければ文字は表示されない。ここからは、tを指定することで明示的に標準出力に出力するようにする。

先程のformatの式の実行後、以下の2行が出力された。

"hello, world"
NIL

この1行目は、tを指定したことによる標準出力への表示が行われた結果表示されたものだ。2行目は、formatの式の実行結果になる。(format nil〜を指定していれば、式の実行結果として文字列が返ってきていたが、(format t〜と指定したことで、式の実行結果として偽、false、空のリストを示すNILが返ってくるようになったことがわかる。

letに話を戻す。letオペレーターは2つの引数を取り、以下のような書式を持つ。

(let argument1 argument2)

第一引数であるargument1は、((message1 "hello") (message2 "world"))で、第二引数であるargument2は、(format nil "~A, ~A" message1 message2)の部分になる。letを詳しく見ていく前に、このような長いコードは適切な位置で改行すると読みやすい。

(let ((message1 "hello")
      (message2 "world"))
  (format t "~A, ~A" message1 message2))

変数名と値の定義は、以下の第一引数のリストで行っている。

((message1 "hello") (message2 "world"))

この記述は、message1という変数名に”hello”という値を、message2という変数名に”world”という値を設定しているという意味になる。複数の変数名と値の組をここに記述し定義することができる。

letオペレーターで定義した変数は、この外のS式からは参照できない局所的に使われる変数であり、この中でのみ有効な変数だ。

formatオペレーターでは、この定義したmessage1、message2という変数名をカンマ区切りで出力するようにしている。結果として、”hello, world”が表示される。

(format t "~A, ~A" message1 message2)

defparameterやdefvarを使った際は、定義した変数名から紐付く値を取得できたが、試しにmessage1やmessage2をREPLに入力してみるとエラーになるはずだ。

CL-USER> (let ((message1 "hello")
               (message2 "world"))
           (format t "~A, ~A" message1 message2))
"hello, world"
NIL
CL-USER> message1
# message1はlet内でのみ利用できるため、エラーになる

どこからでも参照できるスペシャル変数と、このようなletの式内でしか参照できないレキシカル変数では、後者は不便なだけだと考えるかもしれない。

変数の種別によるメリット、デメリットは実際にプログラムコードを記述していくことで理解が深まる部分も多いとは思うが、プログラムの不具合の原因として多くあるもののひとつに、値がプログラマーの想定したものではないという状態が挙げられる。 その原因究明の際、どこからでも参照できるスペシャル変数を利用していた場合は、全ての処理がその変数を書き換える可能性があるためプログラム全体を確認していく必要がある。逆にレキシカル変数によって管理されている値であれば、let式単位で値を確認できるため問題箇所を特定しやすい、管理しやすいという利点が生まれる。 また名前を付ける際も、局所変数であればletの中でのみ有効であるため、重複した名前を使っていないかどうかなど、気にしなければならない事柄が減る。

letオペレーターは、第二引数以降、任意の数の引数を取ることができる。(+ 1 2 3)のように、いくつもの数値を与えられる+オペレーターと似たような形だ。

CL-USER> (let ((message1 "hello")
               (message2 "world"))
           (format t "~A - ~A~%" message1 message2)
           (format t "~A | ~A" message1 message2))
hello - world
hello | world
NIL

レキシカル変数の値を変更する

レキシカル変数も、スペシャル変数と同様、setq、setfによる値の変更が行える。一度定義した値のうち、message2に”lisp”という値をsetfで設定する処理は以下のようになる。

CL-USER> (let ((message1 "hello")
               (message2 "world"))
           (setf message2 "lisp")
           (format t "~A - ~A~%" message1 message2)
           (format t "~A | ~A" message1 message2))
hello - lisp
hello | lisp
NIL

まとめ

このPartでは、定数、変数(スペシャル変数、レキシカル変数)の説明を行った。ここで短くまとめる。

オペレーター 説明 使い方
defconstant 定数定義 (defconstant 名前 値)
defparameter スペシャル変数定義 (defparameter 名前 値)
defvar スペシャル変数定義 (defvar 名前 値)
let レキシカル変数定義 (let ((名前 値) (名前 値)...) ...)
setf 変数への値の代入 (setf 名前 値)
setq 変数への値の代入 (setq 名前 値)