Clozure CL Part2

プログラミングについて、どのように学習していくか。LispのS式、リスト、アトム、シンボルについての入門。

Lispをはじめる前に

Lispをはじめる前に、プログラミングについて改めて説明する。もしプログラミング言語をひとつ以上習得しているのであれば、このページはうんざりするものになるかもしれないので注意。

小さなコードをきちんと理解する

プログラミングは、コンピューターに計算させる行為だ。テキストで行いたい処理を記述し、それをコンピューターが解釈して実行する。エディタの支援などもあるが、一文字ずつ入力し、多大な時間をかけてソフトウェアを作る。問題や壁にぶち当たり、前に進めなくなることもある。困難を乗り越えても、新しい困難が立ちはだかることも多い。

なぜ計算処理によってゲームや普段使用するアプリケーションが出来上がるのかと最初は疑問に思うかもしれない。

中年の配管工がジャンプするための命令などコンピューターには存在しないし、1と1を足して2になります、”hello, world”という文字が出力されます、というような初学者向けのプログラムコードからはじまる入門書の類を読む限り、とても何かが作れるような気はしない。

自分が初めて学んだC言語では、最初に文字を出力するプログラムを書いた。つまり、以下のようなコードだ。

#include<stdio.h>
int main(int argc, char* argv[]) {
  printf("hello, world\n");
  return 0;
}

このようなことを続けて、何か素晴らしいものが作れるとは到底思えない。しかし物事は積み重ねが大事で、プログラムもまた、小さな小さな計算の欠片を積み重ね、組み合わせて作られていく。 単純な計算も組み合わされば、複雑な計算になる。そして複雑な計算は、複雑な処理、複雑な機能を実現する。

だから、小さなコードを退屈に思うことはない。小さなコードが理解できれば、それが積み重なっている巨大なコードも理解できるようになるし、それを自分で書けるようにもなる。

利用できるものは利用する

膨大なコードを書かなければ、作りたいものを実現できないのではないかと怖がる必要もない。ゼロから全てを自分で作る必要はないのだ。

あなたがもしゲームを作りたいと考えたとき、まずは画像を表示することを考えるだろうか。 画像を表示するための計算、音を鳴らすための計算、ネットワーク通信を行うための計算、様々な計算≒様々な処理を簡単にしてくれるためのプログラムの資産(ライブラリと呼ばれる)は既に世の中に存在していることが多い。

自分が実現したい事柄を実現しやすくしてくれるライブラリを探して利用することで、より少ない労力でソフトウェアを作ることができる。

もう少し具体的にプログラミングのことを

Part1では開発環境構築の流れで、REPL上でいくつかのテキストを入力した。

CL-USER>

に続けてテキストを入力したわけだが、ここに入力して結果を表示させることがプログラミングだ。もうひとつの方法として、ファイルにテキストを入力して、それを保存した後に読み込ませて実行し、結果を表示させることもできる。 どちらの方法にしろ、Lispが理解できるテキストを与えて、それを処理させて、結果を得るという部分に変わりはない。

Lispが理解できるテキストとは、プログラムコードのことだ。どのような処理を行うかをひとつひとつ記述していく。その書き方はプログラミング言語によって異なるため、言語が異なれば違う書き方になる。

これから説明していくClozure CLはCommon Lispと呼ばれるプログラミング言語であり、Common Lispのルールに則ってプログラムコードを記述する必要がある。ここで、Common LispとClozure CLは1対1の関係ではないことに注意する。Common Lispは、あくまでCommon Lispというプログラミング言語の記述や挙動のルールを規定した規格であり、その実体としてClozure CLというものが存在するという関係になる。故に、Common Lispには様々な実体≒実装(処理系とも呼ぶ)が存在する。

Clozure CL以外にも以下のような処理系が存在している(もっと他にもある)。

  • SBCL
  • Embeddable Common Lisp
  • GNU Common Lisp

これらは、Common Lispという規格によって実装されている一方、処理系毎に独自の特徴を持ち、また拡張を施しているものが多い。そのため、一方の処理系で動作していたプログラムが、別の処理系では動作しないという問題を引き起こすこともある。

これはCommon Lisp特有のものではない。Rubyと言ったプログラミング言語では、派生があるものの実際には単一の処理系しか存在しないと言っても差し支えない状態のため、このような問題は引き起こさない。しかしJavaScriptでは、ブラウザに備わる実行環境、Node.js、GTMと言った環境の違いによって使用できる構文、機能が異なってくる。

その良し悪しをここで説くことはしない。ここで解説するのは、Common Lispというプログラミング言語規格に沿って実装されている、Clozure CLという処理系におけるプログラミングの方法であるため、他の処理系では問題が起きる可能性があることを覚えておいてほしい。

Common Lispには多数の処理系が存在し、そして世の中にあるプログラミング言語の数はより多く、多様性を持っている。 これらに共通するのは、以下のプロセスだ。

  • プログラムコードを用意する
  • 処理系がプログラムコードを解釈する
  • 処理を実行し、結果を得る

これらのプロセスのうち、プログラムを書くときに着目する点はおおざっぱに言ってしまうと2箇所だけだ。

  • どういうコードを書くのか
  • どういう結果を得たいのか

しかしこの順番は少しおかしい。どういうコードを書くのかは、どういう結果を得たいのかがわかっていなければ導き出せない。考える順番としては、以下の順序が正しい。

  • どういう結果を得たいのか
  • どういうコードを書くのか

中年の配管工をジャンプさせたい。でなければ世界が滅んでしまう。

中年の配管工をジャンプさせるという結果を得るためには、どういうコードを書く必要があるのか。コードは小さな計算の積み重ねであり、そのひとつひとつを書いていくためには、実現したいことを細かく紐解いていく必要がある。

具体的には、以下のように考えていく。

  • 中年の配管工が表示されている必要がある
  • スクリーンに表示させる必要がる
  • ジャンプさせるには地面が必要だ
  • 地面があるなら空も必要だ
  • ボタンをおしたときにジャンプさせたい
  • ボタンをおしていないときには何もさせたくない
  • 中年の配管工は画像を作って表示させる
  • 中年の配管工の画像サイズは….

キリがないように思えるかもしれないが、これは必要なことだ。細かく物事を分解していけば、それは最終的に数値で表現され、計算可能なものになる。画像データでさえ、ただの数値の並びに過ぎない。言い換えると、数値として取り扱えるものは計算することができ、コンピューター上で表現可能だ。

最初から複雑で巨大なものを作り上げることはできない。開発手法や効率化の面では議論の余地もあるだろうが、熟練のプログラマーであっても、小さな部品を作り、積み重ねて大きなものにしていくことに変わりはない。

「昔はこうやってテキストをひとつひとつ入力しながらソフトウェアを作っていたんだよ」と言ったように、プログラミングという行為が伝統工芸になる日が来るかもしれない。実際、コードを記述しない実験的なソフトウェア開発手法は今日までいくつも提案されてきており、将来的にはより効率的なソフトウェア開発の手法が生み出されるだろう。しかし実用的な分野に限っていえば、幸いなことにテキストを入力していくという面倒なやり方しかまだ存在しない。

プログラミングについてまとめよう。

  • どういったことをソフトウェアで実現したいのかを明確にする
  • 実現したいことを細分化する。計算可能な状態にまで分割する
  • 細分化し、分割した実現したいことをプログラムコードとして記述する

実現したいことの明確化は、あやふやな、頭の中に浮かんでいる作りたいものをきちんと言葉にしていくことだ。

細分化、分割は、設計方法論として様々なものがあり、議論される部分も多い。ここでは、作るものに従ってサンプルコードを示していくが、書いてあるものがベストだとは限らない。こう分割しておいたほうが良い、こう設計したほうが良いと思えば、個人個人がそれを実際に試し、結果を自分の目で確認していこう。

最後に、プログラムコードとして記述する部分だが、これは単純な学習によって習熟できるもっとも簡単な部分だ。どういう処理を行うべきかが完全に頭の中にあり、コードを記述するための少ないルールを覚えていれさえすれば、それをコードとして表現することは容易い。

まずは取り掛かりやすいプログラムコードの記述方法の説明から始める。

Lispをはじめよう

オペレーターと引数

Lispは、括弧の中に色々と書いていく。以下のように、開き括弧と綴じ括弧を書いて、その中に処理を書く。

CL-USER> (+ 1 2)
3

上記のコードは足し算で、1と2を足す処理だ。そのため、入力してENTERキーを押すと3という数字が出力される。算数の世界であれば、1 + 2という書き方だが、Lispでは+ 1 2と書く。足し算の記号が一番前にあり、その後ろに数字が並ぶ。

引き算にはマイナス記号を使う。

CL-USER> (- 2 1)
1

1 + 2 + 3はどう書くと良いだろうか。答えは以下のようになる。

CL-USER> (+ 1 2 3)
6

括弧の先頭にある要素が、後ろに続く数字の計算方法を決めていることに気付くだろうか。先頭の要素はオペレーター(operator)と呼び、その後ろに続く要素を引数(argument)と呼ぶ。重要なことは、+だからオペレーターなのではなく、先頭にあるからオペレーターだと言うことだ。Lispは、問答無用で先頭にある要素をオペレーターとして解釈する。解釈しようと試みる。 +は加算のオペレーターとしてLispが理解しているため、足し算になる。-は減算のオペレーターとしてLispが理解しているため、引き算になる。

では、1を先頭に持ってきたらどうだろう。オペレーターとして解釈されるのだろうか?

CL-USER> (1 + 2 3)

このコードは、Lisp側で処理できずエラーになる。 Lispは1をオペレーターとして解釈しようと試みるものの、残念ながら1というオペレーターはLispに備わっていない。備わっていないということは、2番目以降に続く要素をどのようにオペレーション(操作)して良いかが、Lispにはわからないということだ。その結果として、エラーになる。

Lispに備わっている四則演算用オペレーターのうち、掛け算と割り算を見ていこう。掛け算は*を使い、割り算は/を使う。

CL-USER> (* 3 4)
12
CL-USER> (/ 6 2)
3

Lispが知っているオペレーターを使う限り、問題なく計算されることが確認できる。 オペレーターには四則演算以外にも様々なものが用意されており、Lispが予め備えているものに加えて、プログラマーが自分で作ることもできる。+-などの記号だけではなく、アルファベットを使いわかりやすい名前で作ることができる。 Lispに備わっているアルファベットのオペレーターとして、文字を出力するformatというオペレーターを使ってみる。

CL-USER> (format nil "~A" "hello, world")
"hello, world"

括弧の先頭はオペレーター、2番目以降に続く要素は引数ということをまずは覚えよう。このようなLispで解釈する式を、S式と呼ぶ。

プログラミングは小さなコードの欠片を組み合わせて、複雑なものを作り上げていく。単一の括弧の組の中に、オペレーターと引数を指定して計算や文字の出力を行ったが、括弧の中に括弧を入れるといった形で、より複雑な処理を行うことができる。

1 + 2 + 3は、(+ 1 2 3)と書いたが、これは1と2を足した後、その結果を3と足すという言葉に置き換えることができる。これをREPLでそのまま書くと2回の入力に分けることができる。

CL-USER> (+ 1 2)
3
CL-USER> (+ 3 3)
6

この2つの括弧を、1つの式にするためには、括弧の中に括弧を入れる形で表現する。具体的には、以下のように記述する。

CL-USER> (+ (+ 1 2) 3)
6

はじめて見ると混乱するかもしれない。このように括弧が二重以上になっている場合の処理は、必ず一番内側の括弧内から見ていく。この例の一番内側の括弧は、(+ 1 2)だ。先程のREPL上での入力でも結果を得ているが、(+ 1 2)の結果は3なので、(+ 1 2)は3そのものだと言える。 つまり、内側の括弧内を計算すると、二重になっていた構造は、以下のように単一の括弧の状態になる

(+ (+ 1 2) 3) → (+ 3 3)

最初の段階では、+というオペレーターに対して、2番目の要素は(+ 1 2)、3番目の要素は3が与えられている。(+ 1 2)が計算されて3という数字になると、2番目の要素は3、3番目の要素は3という状態になる。

Lispがどのようにコードを解釈しているか、という点から考えたほうがわかりやすいかもしれない。先頭からひとつひとつ見ていく。 以下の表は、(+ (+ 1 2) 3)を1文字ずつ切り出したものだ。それぞれの段階で、Lispがどのように解釈して計算を行うかを説明する。

Lispの気持ち
( 開き括弧があるので、LispのS式がはじまる。次にある要素をオペレーターとして取り扱おう。
+ +記号がある。これは加算のオペレーターだ。以降に続く要素は加算していくぞ。加算だからきっと数値の要素が来るはずだ。
( また開き括弧だ。これは入れ子のS式なんだな。まずはこの内側の括弧内を優先して計算しよう。次にある要素をオペレーターとして取り扱おう。
+ 加算のオペレーターを発見。続く要素は全部足すぞ。数値が来るはずだ。
1 数値の1が来た。1と何を足すのかな。
2 数値の2が来た。1と2を足すのはわかったけど、まだ何か足すものはあるかな?
) 閉じ括弧だ。この内側の括弧は終了した。+に続く要素は1と2だけだったので、それらを計算して3という結果が出た。この結果を、外側の括弧の+オペレーターの引数として取り扱おう。この3と、何を足すのかな。
3 数値の3が来た。3と3を足すのはわかったけど、まだ何か足すものはあるのかな?
) 閉じ括弧ということはここで外側の括弧も終わり。つまり計算はこれ以上ないから、結果を出そう。

というような流れになる。もし処理の順序が理解しづらかったり、どうしてこう動くのかがわからないといった部分がある場合には、Lispの気持ちになって先頭からひとつひとつ読み進めていくことを勧める。

このような入れ子の書き方は、複数種類のオペレーターを使いたいときに必須となる。例えば、足し算と掛け算を行う計算式を表現したい場合だ。4 + 2 × 7という計算を行いたいとする。算数では、掛け算が優先されるため、2 × 7が最初に計算されて14という数値を得る。その後、4 + 14を計算し、18という結果が得られる。

こういう場合に、先頭の要素だけがオペレーターとして取り扱われるため、単一の括弧では足し算と掛け算の2つを用いることができない。Lispのプログラムコードでこの計算式を表現するためには、まず2と7を掛け合わせる式を用意する。

CL-USER> (* 2 7)
14

次に、これに4を足す処理を加える。注意する点として、括弧が入れ子になっている場合は内側から計算されていくので、掛け算である(* 2 7)という記述は一番内側になくてはならない。Lispは、算数における四則演算の優先順位をまったく考慮せず、必ず内側から計算を行おうとするからだ。4を加算させるためには、以下のように記述する。

CL-USER> (+ 4 (* 2 7))
18

もしくは、順番を逆にして以下のようにも書ける。

CL-USER> (+ (* 2 7) 4)
18

どちらも、掛け算である内側の括弧内が最初に処理され、その後、足し算が行われる。 ここまでのことをまとめる。

  • Lispは()で囲まれたS式を処理する
  • 括弧内の先頭にある要素はオペレーターと呼ぶ
  • 括弧内の2番目以降にある要素は引数と呼ぶ
  • オペレーターに基づいて、Lispは2番目以降の要素を処理する
  • 括弧の中に更に括弧を記述できる。
  • 必ず一番内側の括弧内から計算処理が行われる。

リスト

LispはLISt Processorの略だ。その名に含まれるように、Lispのコードはリストによって表現される。今まで見てきた、括弧の中に要素が並ぶ記述そのものが、リストと呼ばれるものだ。リストの中には、アトムとリスト以外含まれない。アトムという呼び方は聞きなれないが、数値、文字列、シンボル(記号)の3つを指し示す。

(+ 1 2 3)

というS式であれば、+123は全てアトムと呼ばれるものになる。S式全体となる(+ 1 2 3)はリストだ。

(+ 4 (* 2 7))

というS式であれば、一番外側のリストは、+4というアトム、(* 2 7)というリストを含んでいる。内側のリストである(* 2 7)に含まれるのは、*27というアトムになる。

最後に、文字出力のためのformatオペレーターを使ったS式を見る。

(format nil "~A" "hello, world")

format、nil、”~A”、”hello, world”の全てがアトムになる。format、nilはシンボルであり、”~A”、”hello, world”は文字列だ。

以下の表でコードの構成要素を整理する。

リストの構成要素 分類 説明
アトム 数値 1,2,3,4,5のような数値
文字列 “hello”のように二重引用符で囲んだ文字列
シンボル +,-,format,nilのような記号やアルファベットで表現される名前
リスト ()で囲まれた要素

どのようなルールで記述するか、言い換えると、どのような構文で記述するのかは、プログラミング言語を学ぶ際一番最初に覚えなくてはいけない部分だ。 この点において、Lispは非常にシンプルな構文を持つ。構文がないと言ってしまっても差し支えないほどに、他の言語と比べると特殊な作りをしている。

ここまでの内容を理解しても、プログラムを書ける気にはならないかもしれない。電卓レベルのことしかしていない。実際、変数や関数、分岐や繰り返し処理などを覚えなければ、まともなプログラムは書けない。しかしそれらの様々な機能を覚える際も、書き方は今説明したとおりで、これ以上のことは出てこない。 この段階では、Lispのコードはアトムとリストで構成され、内側から処理されるという簡潔なルールを覚えておこう。

もしここまでのプログラムコードを入力してエラーが出てしまう場合には、以下の点を確認すると良い。

  • 括弧はきちんと組で使われているか。開き括弧や綴じ括弧がきちんと対応しているか。
  • リスト(括弧内)の先頭は、Lispが解釈できるオペレーターかどうか。
  • リスト内には、リストかアトムしか含めることができない。それ以外のものが含まれていないか。
  • オペレーター毎に一定のルールで引数を処理する。オペレーターに与えている引数の値(数値や文字列、シンボルなど)はサンプルコードと同じかどうか。