前回はリスト、シンボル、文字列、整数値といった Scheme で使用するデータの種類と、関数の使い方について説明しました。今回は「変数 (variable)」と「評価 (evaluation)」について説明します。
前回は関数を呼び出すとき、実引数の値が仮引数 (シンボル) に代入されることを説明しました。シンボルに値を代入するのは、関数呼び出しのときだけではありません。特殊形式 def を使うと、シンボルを変数として定義し、そこに値を代入することができます。
def symbol-name s-expr
それでは、実際に def を使ってシンボルに値をセットしてみましょう。
user=> v Syntax error compiling at (REPL:0:0). Unable to resolve symbol: v in this context user=> (def v 10) #'user/v user=> v 10
最初の実行結果を見て下さい。プロンプトが表示されている状態からシンボル名を入力すると、そこに格納されている値を表示します。この例では v に値がセットされていないので、エラーが表示されました。Clojure が起動されたときには、特別なシンボルを除いて、シンボルに値は設定されていません。値が定まっていないシンボルにアクセスしようとすると、このようなエラーが発生します。
次に、def でシンボルに値をセットします。def には第 1 引数にシンボル、第 2 引数にセットする値を渡します。def の返り値はシンボルとリンクされた Var Object になります。def は特殊形式なので、第 1 引数のシンボルは評価しないことに注意してください。def は第 1 引数をそのまま受け取り、第 2 引数を評価した結果をシンボルに代入します。たとえば、第 2 引数にリストを書けば、その実行結果がシンボルに代入されます。
user=> (def v1 (+ 1 2 3)) #'user/v1 user=> v1 6 user=> (def v2 v1) #'user/v2 user=> v2 6
2 番目の例のように、第 2 引数がシンボルであれば、そこに格納されている値が第 1 引数のシンボルに代入されます。
ところで、Common Lisp や Scheme の変数は mutable なので、値を書き換えることができます。Common Lisp では特殊形式 setq, setf で、Scheme であれば set! を使います。元の値を書き換えてしまうことを「破壊的」 [*1] といいます。逆に、値を変更しない操作は「非破壊的」といいます。値を読み取る操作は非破壊的です。
Clojure の場合、変数は基本的に immutable なので、値を書き換えることはできません。ただし、def は同じシンボルに対して何度も適用することが可能です。REPL で試してみましょう。
user=> v 10 user=> (def v 100) #'user/v user=> v 100 user=> (def v 1000) #'user/v user=> v 1000
今、v の値は 10 になっています。このあと、(def v 100) を評価すると、v の値は 100 になり、さらに (def v 1000) を評価すると、v の値は 1000 になります。def を評価すると新しい Var Object を生成し、それとシンボルをリンクします。つまり、変数の再定義を行っているのですが、見た目は変数の値を書き換えたのと同じ結果になります。
REPL でプログラムを試してみるときには便利ですが、あまりお行儀のよいプログラムとは言えません。ML 系の言語 (SML/NJ や OCaml など) には値を更新できる参照型の変数が用意されています。Clojure にも同等の機能が用意されているので、破壊的な操作が必要なときは、そちらを使うことをおススメします。
ところで、関数を実行する場合、仮引数に値が代入されますね。代入は破壊的な操作ですから、仮引数の元の値は書き換えられてしまうはずです。ところが、前回説明したように、関数の実行が終了すると、仮引数として使用された変数の値は元に戻ります。
それでは、前回定義した関数 square を使って、実際に確かめてみましょう。
square の定義 : (defn square [x] (* x x))
user=> (def x 10) #'user/x user=> x 10 user=> (square 20) 400 user=> x 10
最初に def で x に 10 をセットします。次に、(square 20) を実行します。このとき、仮引数 x に 20 がセットされ、square が実行されますね。その結果 (* 20 20) が計算されて 400 という値が返されます。そして、実行終了後に x の値を確かめてみると、最初にセットした値 10 のままです。
このように、仮引数として使用される変数は、その関数が実行されている間だけ有効なのです。このような変数を「局所変数 (local variable)」といい、それ以外の変数を「大域変数 (gloabl variable)」といいます。
最初 def で変数 x に値をセットしましたが、def は x を大域変数として扱います。関数が実行されるとき、x は仮引数として使用されているので、局所変数として扱われます。
┌───── Clojure system ───────┐ │ │ │ 大域変数 x │ │ 大域変数 y ←───────┐ │ │ │ │ │ ┌─ 関数 foo 仮引数 x ─┐ │ │ │ │ ↑ │ │ │ │ │ ┌────┘ │ │ │ │ │ (println x) │ │ │ │ │ ┌──────┼─┘ │ │ │ (println y) │ │ │ │ │ │ │ └────────────┘ │ │ │ └────────────────────┘ 図 : 大域変数と局所変数の関係
上図を見てください。関数 foo で変数 x, y の値を println で表示します。println は引数を画面に表示したあと、改行文字を出力します。変数 x は関数 foo の仮引数です。この場合、x は局所変数として扱われます。変数 y は仮引数ではないので大域変数として扱われます。実際に試してみると、次のようになります。
user=> (def x 10) #'user/x user=> (def y 20) #'user/y user=> (defn foo [x] (println x) (println y)) #'user/foo user=> (foo 1000) 1000 20 nil
局所やら大域やら難しい話になりましたが、この違いはプログラムを作成する場合、とても重要になります。とくに、局所変数の機能は、関数を部品のように使うためには必須の機能なのです。まあ、実際にプログラムを作るようになると、すぐに理解できることなので心配は無用です。
Lisp 言語の変数は、どのようなデータ型でも格納することができます。ほかのプログラミング言語では、変数に格納するデータの種類をあらかじめ決めておかなければならないものがあります。たとえばC言語の場合、変数 a に整数値を格納する場合は int a というように、a に格納するデータの種類を決めます。そして、a には整数値以外のデータを格納することはできません。
Lisp 言語の場合、変数 a には整数値、文字列、シンボル、リストなど S 式であれば何でも格納することができます。ところで、整数値は def で変数に代入できましたが、シンボルやリストを変数に代入することができるのでしょうか。def の第 2 引数は「評価」されることを思い出してください。
user=> (def x 10) #'user/x user=> x 10 user=> (def y x) <-- 引数 x が評価され 10 が y に代入される #'user/y user=> y 10
変数 y にシンボル x を代入する場合、def にそのまま x を与えると、x が評価されてその値が y に代入されてしまいます。リストの場合は、それがプログラムとして実行されるので、リスト自身を変数に代入することはできません。シンボルやリストを変数に代入するときは、引数が評価されては困るのです。そのため、引数を評価しないようにする関数が用意されています。次の例を見てください。
user=> (def y 'x) #'user/y user=> y x user=> (def y '(1 2 3 4)) #'user/y user=> y (1 2 3 4)
引用符 ' をつけると、その次の S 式は評価されません。引用符は関数 quote の省略形で、'x は Clojure によって、(quote x) と変換されます。quote は特殊形式で、引数を評価せずにそのまま返す働きをします。したがって、(def y 'x) の場合、(quote x) が評価されて x 自身が返り値となります。つまり、シンボル x に格納されている値が取り出されるのではなく、x 自身が関数に渡されるのです。その結果、変数 y にシンボル x を代入することができます。
同様に、リストの場合も引用符をつけることで、変数に代入することができます。この場合、リストは評価されない、つまり、プログラムとして実行されないので、最初の要素が関数である必要はありません。'(1 2 3 4) は (quote (1 2 3 4)) に変換され、それが評価されて (1 2 3 4) というリスト自身が関数 def に渡されるのです。
この場合、リストはプログラムではなくデータとして扱うことになります。リストにプログラムとデータという 2 つの役割を持たせていることが、ほかの言語とは最も異なる Lisp 言語の特徴です。
特殊形式以外の関数は引数を必ず評価するので、リストやシンボル自身をデータとして扱うために quote を頻繁に使うことになります。いちいち (quote (1 2 3 4)) と書いていては面倒だし、プログラムが読みにくくなってしまいます。そこで、'(1 2 3 4) のような省略形が使われるようになりました。
なお、シンボルとリスト以外のデータは、評価されても自分自身になる自己評価フォームですから、数値や文字列には引用符を付ける必要はありません。
さて、これでリストをプログラムだけではなく、データとして扱うことができるようになりました。Lisp の由来である「LISt Procseeor」からもわかるように、Lisp 言語はリストを扱うことが得意のプログラミング言語です。ここでリスト操作の基本関数を説明しましょう。
(1) first list : リストの先頭の要素を取り出します。 (2) rest list : リストの先頭要素を取り除いたリストを返します。
┌─────→ first は先頭のセルの CAR 部を返す │ │ ┌───→ rest は先頭のセルの CDR 部を返す │ │ ┌─┬─┐ ┌─┬─┐ ┌─┬─┐ ┌─┬─┐ │・│・┼─→│・│・┼─→│・│・┼─→│・│/│ └┼┴─┘ └┼┴─┘ └┼┴─┘ └┼┴─┘ ↓ ↓ ↓ ↓ a b c d (first '(a b c d)) => a (rest '(a b c d)) => (b c d) 図 : first と rest の操作
伝統的な Lisp では、first のかわりに car を、rest のかわりに cdr を使います。first と rest は関数ですので、リストには引用符をつけることを忘れないでください。また、伝統的な Lisp では、リスト以外のデータを与えるとエラーとなりますが、Clojure では「列 (sequence) 型データ」にも適用 [*2] することができます。本ページではリストでの動作を説明します。
first と rest はリストを分解します。上図に示すように、first は先頭のセルの CAR 部に格納されたデータを返します。rest は CDR 部に格納されたデータ (後ろに接続されているセル) を返します。つまり、先頭要素を除いたリストを返すことになります。
それでは、要素がひとつしかないリストの rest はどうなるでしょうか。実際に試してみましょう。
user=> (rest '(a)) ()
rest は先頭の要素を取り除いたリストを返しますが、それはセルの CDR 部を返す働きと同じです。前回で、最終セルの CDR にはリストの終わりを示す特別なデータが格納されることを説明しました。したがって、要素がひとつしかないリストに rest を適用すると、リストの終端を表すデータが取り出されます。もうおわかりだと思いますが、この特別なデータが () なのです。() は「空リスト (empty list)」を表すリスト型 [*3] のデータです。
┌─┬─┐ ┌─┬─┐ ┌─┬─┐ │・│・┼─→│・│・┼─→│・│・┼─→ () └┼┴─┘ └┼┴─┘ └┼┴─┘ リストの終端 ↓ ↓ ↓ a b c リスト (a b c) の構造 図 : リストの終端
今までのリストは、すべて終端に () がセットされています。
なお、空リストに first (car), rest (cdr) を適用した場合、Lisp 言語によって動作が異なります。
: first (car) | rest (cdr) ------------+-------------+------------ Clojure | nil | nil Common Lisp | NIL | NIL Scheme | エラー | エラー
Scheme は空リストに car, cdr を適用するとエラーになりますが、Clojure と Common Lisp は nil を返します。ここで、Clojure と Common Lisp では nil の意味が異なることに注意してください。Common Lisp の nil はシンボルですが、空リストも表しています。Clojure の nil は空リストではなく、値が無いことを表す特別なデータです。Clojure の場合、リストから要素を取り出すことができないとき、エラーではなく nil を返すように設計されているようです。
もう少し first と rest の例を見てみましょう。
(first '((a b) (c d) (e f))) => (a b) (rest '((a b) (c d) (e f))) => ((c d) (e f))
リストの要素は (a b) であり、a ではないことに注意してください。a を取り出したい場合は first を 2 回適用します。
(first (first '((a b) (c d) (e f)))) => a
最初の first の引数はリスト (first '((a b) (c d) (e f))) ですから、この S 式が評価されて、(a b) という結果になります。このリストが最初の first に適用されて a を取り出すことができるのです。なお、Clojure には first を 2 回適用する関数 ffirst が用意されています。伝統的な Lisp では caar があります。
それでは、2 番目の要素 (c d) を取り出す場合はどうするのでしょう。この場合は、first と rest を組み合わせれば簡単に実現できます。
(first (rest '((a b) (c d) (e f)))) => (c d)
最初の first の引数はリスト (rest '((a b) (c d) (e f))) ですから、この S 式が評価されると先頭の要素 (a b) が取り除かれて、((c d) (e f)) という結果になります。このリストが最初の first に適用されて (c d) を取り出すことができるのです。このように、first と rest を組み合わせることで、リストのどの要素にもアクセスすることができます。もっとも、Clojure には便利な関数や機能 (分配束縛) が用意されているので、もっと簡単にアクセスすることができます。
次は、リストの合成を行う関数を説明します。
(3) cons x ys : CAR 部に x を CDR 部にリスト ys をセットしたセルを返します。 (4) list [args] : args を要素とするリストを返します。 (5) concat [lists] : 引数のリストを連結して返します。
cons は新しいコンスセルを生成し、そのコンスセルの CAR 部に第 1 引数 x を CDR 部に第 2 引数 y をセットします。x がデータで y がリストの場合、cons はリスト y の先頭にデータ x を追加したリストを返します。
新しいセル ┌─┬─┐ cons の返り値 ←─│・│・┼─→ ()(引数) └┼┴─┘ ↓ a(引数) (cons 'a '()) => (a) 新しいセル ┌─── 引数 (b c) ─────┐ ┌─┬─┐ │ ┌─┬─┐ ┌─┬─┐ │ cons の返り値 ←─│・│・┼─┼→│・│・┼─→│・│/│ │ └┼┴─┘ │ └┼┴─┘ └┼┴─┘ │ ↓ │ ↓ ↓ │ a(引数) │ b c │ └──────────────┘ (cons 'a '(b c)) => (a b c) 図 : cons の動作
また、引数 x はリストでも大丈夫です。
(cons '(a b) '(c d)) => ((a b) c d)
first と rest で分解したリストは cons で合成することができます。
┌───┐ ┌─→│first │→ a ────┐ │ └───┘ ↓ │ ┌────┐ (a b c d) ─┤ │cons│→ (a b c d) │ └────┘ │ ┌───┐ ↑ └─→│ rest │→ (b c d) ─┘ └───┘ 図 : リストの分解と合成
この関係は、リストを操作する関数を作る場合の基本です。とても重要な関係なので、覚えておいてください。
新しいセル 新しいセル 新しいセル ┌─┬─┐ ┌─┬─┐ ┌─┬─┐ list の返り値 ←─│・│・┼─→│・│・┼─→│・│/│ └┼┴─┘ └┼┴─┘ └┼┴─┘ ↓ ↓ ↓ a b c (list 'a 'b 'c) => (a b c) 新しいセル 新しいセル 新しいセル ┌─┬─┐ ┌─┬─┐ ┌─┬─┐ list の返り値 ←─│・│・┼─→│・│・┼─→│・│/│ └┼┴─┘ └┼┴─┘ └┼┴─┘ ↓ ↓ ↓ (a b) (c d) (e f) (list '(a b) '(c d) '(e f)) => ((a b) (c d) (e f)) 図 : list の動作
関数 list は、その引数を要素とする新しいリストを返します。上図のように、list は引数を新しいリストの要素として格納します。引数がリストの場合は、それが要素となります。リスト同士を繋ぐのではないことに注意してください。
これに対し、concat はリスト同士を接続します。したがって、引数はリストでなければいけません。アトムを与えるとエラーになります。
引数 (a b) 引数 (c d) ┌─┬─┐ ┌─┬─┐ ┌─┬─┐ ┌─┬─┐ │・│・┼─→│・│/│ ┌→│・│・┼─→│・│/│ └┼┴─┘ └┼┴─┘ │ └┼┴─┘ └┼┴─┘ ↓ ↓ │ ↓ ↓ a b │ c d ↓ ↓ │ ┌┼┬─┐ ┌┼┬─┐ │ ┌──│・│・┼─→│・│・┼─┘ │ └─┴─┘ └─┴─┘ │ 新しいセル 新しいセル ↓ concat の返り値 (a b c d) (concat '(a b) '(c d)) => (a b c d) 図 : concat の動作
concat はリストを接続する場合、新しいセルを作成して引数の要素を格納します。ただし、最後の引数については、上図のように新しいセルを生成しません。要素自体には何もしないことに注意してください。
(concat '((a b) (c d)) '(e f)) => ((a b) (c d) e f) (concat '((a b) (c d)) '((e f))) => ((a b) (c d) (e f)) (a b c d e f) ではないことに注意
このように、要素がリストの場合でも、そのリストの要素を取り出すことはせずに、引数の要素を並べるだけです。それから、concat は複数のリストを引数に与えることもできます。
(concat '(a b) '(c d) '(e f)) => (a b c d e f) (concat '(a) '(b c) '(d e) '(f g)) => (a b c d e f g)
この場合、引数のいちばん最後のリストについては、新しいセルを生成しません。それ以外の引数に対して、新しいセルを生成することになります。最初の例では、(a b), (c d) について新しいセルを生成します。次の例では、(a), (b c), (d e) について新しいセルを生成します。
今まで説明した関数は変数の値を変更しません。
user=> (def v '(a b c d)) #'user/v user=> (rest v) (b c d) user=> v (a b c d) user=> (first v) a user=> v (a b c d) user=> (cons 1 v) (1 a b c d) user=> v (a b c d) user=> (list v v) ((a b c d) (a b c d)) user=> v (a b c d) user=> (concat v v) (a b c d a b c d) user=> v (a b c d)
Clojure の場合、関数定義の仮引数は次に示す 3 通りのパターンがあります。
1 は今までの関数呼び出しと同じ形式で、3 個の仮引数 a, b, c があります。この場合、実引数も 3 個必要になります。2 は &で仮引数を区切っていて、仮引数 a, b, c は 1 と同じですが、& の後ろの引数 args には残りの引数がリストに格納されて渡されます。つまり、3 個以上の引数を受け取ることができます。
3 のように [& args] だけの場合、与えられた引数すべてがリストに格納されて args に渡されます。引数がない場合、args は nil になります。つまり、0 個以上の引数を受け取る関数になります。
簡単な例を示しましょう。
user=> (defn foo [a b c & args] (list a b c args)) #'user/foo user=> (foo 1 2 3) (1 2 3 nil) user=> (foo 1 2 3 4) (1 2 3 (4)) user=> (foo 1 2 3 4 5 6) (1 2 3 (4 5 6)) user=> (defn bar [& args] args) #'user/bar user=> (bar) nil user=> (bar 1 2 3 4 5) (1 2 3 4 5)
このように、可変個の引数を受け取る関数を定義することができます。
Clojure は引数の個数によって関数の動作を変更することができます。これを「マルチアリティ関数」といい、Scheme の case-lambda と同等の機能です。
(defn func-name (formals1 expr1 ...) ... (formalsN exprN ...)) 図 : マルチアリティ関数の構文
formals は仮引数を格納するベクタと同じ形式です。マルチアリティ関数は実引数と formals を順番に照合し、マッチングする節を選択して本体 expr を評価します。マッチングする節が無い場合はエラーになります。簡単な例を示しましょう。
user=> (defn foo ([a] (list 'one a)) ([a b] (list 'two a b))) #'user/foo user=> (foo 1) (one 1) user=> (foo 1 2) (two 1 2) user=> (foo 1 2 3) Execution error (ArityException) at user/eval139 (REPL:1). Wrong number of args (3) passed to: user/foo
(foo 1) は最初の節 ([a] ...) とマッチングするので、返り値は (one 1) となります。(foo 1 2) は 2 番目の節 ([a b] ...) とマッチングするので、返り値は (two 1 2) となります。(foo 1 2 3) はマッチングする節が無いのでエラーとなります。
マルチアリティ関数を使うと、関数の引数にデフォルトの値を設定することができます。Python などのスクリプト言語では「デフォルト引数」といい、Common Lisp では「オプションパラメータ (&optional)」といいます。簡単な例を示します。
user=> (defn foo ([a] (foo a 10 100)) ([a b] (foo a b 100)) ([a b c] (println a b c))) #'user/foo user=> (foo 1) 1 10 100 nil user=> (foo 1 2) 1 2 100 nil user=> (foo 1 2 3) 1 2 3 nil
関数 foo() は引数 a, b, c の 3 つがありますが、b と c にデフォルト値を設定しましょう。引数がひとつの関数を定義します。その処理で (foo a 10 100) を呼び出します。これで (foo 1) を評価すると、この節が選択されるので、1 10 100 と表示されます。
同様に、引数が二つ (a, b) の関数を定義し、その処理で (foo a b 100) を呼び出します。これで (foo 1 2) を評価すると、1 2 100 と表示されます。最後に、引数が三つ (a, b, c) の関数を定義します。前の節で呼び出した foo はこの節とマッチングします。したがって、(foo 1 2 3) を評価すれば、1 2 3 と表示されます。
ご参考までに、Common Lisp (SBCL) の実行例を示します。
* (defun foo (a &optional (b 10) (c 100)) (list a b c)) FOO * (foo 1) (1 10 100) * (foo 1 2) (1 2 100) * (foo 1 2 3) (1 2 3)
今回はここまでです。最後に、今まで説明したことについて、簡単に復習しておきましょう。
次回は、実行する処理を条件によって選択する方法と、同じ処理を何回も繰り返す方法を説明します。この方法を理解すると、おもしろいプログラムを作ることができるようになります。お楽しみに。
user=> (def xs (list 'a (list 'b) (list (list 'c)) (list (list (list 'd))))) #'user/xs user=> xs (a (b) ((c)) (((d)))) user=> (def ys (list (list 'a 'b 'c) (list 'd 'e 'f) (list 'g 'h 'i))) #'user/ys user=> ys ((a b c) (d e f) (g h i)) user=> (def zs (list 'a (list 'b (list 'c (list 'd) 'e) 'f) 'g)) #'user/zs user=> zs (a (b (c (d) e) f) g) user=> (first xs) a user=> (first (nth xs 1)) b user=> (first (first (nth xs 2))) c user=> (first (first (first (nth xs 3)))) d user=> (first (first ys)) a user=> (nth (first ys) 1) b user=> (nth (first ys) 2) c user=> (first (nth ys 1)) d user=> (nth (nth ys 1) 1) e user=> (nth (nth ys 1) 2) f user=> (first (nth ys 2)) g user=> (nth (nth ys 2) 1) h user=> (nth (nth ys 2) 2) i user=> (first zs) a user=> (first (nth zs 1)) b user=> (first (nth (nth zs 1) 1)) c user=> (first (nth (nth (nth zs 1) 1) 1)) d user=> (nth (nth (nth zs 1) 1) 2) e user=> (nth (nth zs 1) 2) f user=> (nth zs 2) g