Clojure(というかLisp)での「評価」について

Posted by YpsilonTAKAI On 2013年12月19日木曜日 0 コメント
最近、Clojureのとっかかりの部分についての説明が書かれたものをWeb上でよく見かけるようになりましたが、Lispを語る上で欠かせない「評価」という事柄についての記述が無いものがほとんどのようです。

Lispの力の源はevalであって、evalとは「評価」そのものであるので、Lispを知る(=使う)には、「評価」を理解することは必須だと思うのです。

あまりえらそうな事を言えるほどのスキルがあるわけではありませんが、ちまちま書いていたものが形になったので、公開します。


最近LispはClojureしか触ってないので、実例にはClojureを使いますが、話し
たいことについては他のLispでもほぼ同じ考えかたをします。

Clojureのプログラム実行とはS式を「評価」することです。

S式については、Lispの式として正しい文字列のことであるということ以上には深く突っ込みません。

さて、数そのものはS式です。REPLのプロンプトに数字を入れてリターンを押すとどうなるでしょう。

user> 41
41

リターンを押すと、REPLは与えられたS式を評価します。数は評価されると自分自身を返します。どんな数でも同様です。

user> 3.14
3.14
user> 355/113
355/113
user> 6.02e23
6.02E23

数字もちゃんと評価されているんだということは重要です。例外は無いのです。
文字も文字列も自分自身を返します。

user> \a
\a
user> "hello"
"hello"

他の言語での変数に相当するシンボルも、評価されます。

user> a
CompilerException java.lang.RuntimeException: Unable to resolve
symbol: a in this context, compiling:(NO_SOURCE_PATH:1:1208)

エラーになりました。シンボルは評価されると、そのシンボルに束縛されてい
るものが返ります。「束縛」というのは、ここではとりあえず「代入」と同じようなも
のだとしておきます。

シンボルに何かを束縛するには、いくつかの方法があります。
defを使うと、そのネームスペース(ここではuser)内で有効な束縛を作れます。

user> (def a 42)
#'user/a

このようにaに42を束縛したので、aを評価するとこんどはその42が出てきます。

user> a
42

リストの評価


「(def a 42)」は、リストです。Lispでは、リストを評価したとき、それは関数呼び出しとして処理され、以下のようなことが行なわれます。

関数呼び出しでは、リストの1番目の要素を評価し、「それ」にリストの残りを引数として渡してその返り値をリストの評価の結果とする。

です。これは、Lispの決まりなので、どのようなLispでも同様です。

さて、ここで「それ」と書きましたけど、これは、「引数を取って値を返すもの」で、大きくわけて2種類があります。

  • 関数
  • マクロと特殊形式

この2種類は、評価時の引数の評価の方式が異なります。

defはこの中の特殊形式に属します。特殊なものは後まわしにして、

まずは関数から説明します。

関数の引数の評価は、関数に引数が渡される「前」に行なわれます。

関数の代表として、「+」を見てみます。Lispでは「+」も演算子ではなく、関数として実装されています。

user> (+ 10 3)
13

そして、この「+」という記号もシンボルです。 評価してみましょう。

user> +
#<core$_PLUS_ clojure.core$_PLUS_@58943431>

変なものが返りました。 ですが、これはエラーメッセージではありません。+ に束縛されている関数そのものが表示されているのです。でも、関数の実体そのもは表示できない(できたとしても意味が無い)ので、その印字表現が出力されているのです。Java的に言うと、クラスの持っているtoStringの結果が返るようなものです。

引数の10は10に、3は3に評価されます。
user> 10
10
user> 3
3

ということで、(+ 10 3) を評価すると、

#<core$_PLUS_ clojure.core$_PLUS_@58943431> という関数に、10を評価した10と、3を評価した3が渡され、この関数は、与えられた引数を足すものなので、その結果の13が得られ、それが(+ 10 3)の返り値になる。

となります。
xに10が、yに3が束縛されている場合に、(+ x y)とした場合はどうでしょう。

user> x
3
user> y
10
user> (+ x y)
13

同じく13が返ります。

ここで注目すべきは、引数がx,yのようなシンボルでも、10,3のような数値でも、評価されるという点では同じであるということです。そしてこれは、引数がリストである場合でも同様です。

user> (* (+ 2 4) (- 20 5))
90

出てくる関数はそれぞれ

user> *
#<core$_STAR_ clojure.core$_STAR_@a8b7b53>
user> -
#<core$_ clojure.core$_@66ed489f>

です。

この式(* (+ 2 4) (- 20 5))は、
#<core$_STAR_ clojure.core$_STAR_@a8b7b53>という関数に、(+ 2 4)を評価した値と(- 20 5)を評価した値が渡されて、結果の90が得られ、それが、全体の返り値となっています。そして、(+ 2 4)や(- 20 5)も「評価」されるわけですから、同様な処理を行なってそれぞれの返り値が生成されるという具合です。

user> (+ 2 4)
6
user> (- 20 5)
15

このように、Lispの処理はリストの評価で開始され、すべての要素を評価して、先頭の関数に残りを引数として渡すことで進みます。 評価が再帰的に適用されて、伝搬していくのです。

ところが、そこにあるものを全て評価してしまいたくないこともあるのです。

次にマクロと特殊形式の場合を見てみましょう。

「マクロと特殊形式」と、今見てきた「関数」では、引数の評価の方針が異なります。
関数の場合、引数は関数に渡される「前」に評価されていましたが、これらの場合は、引数を評価するかどうかは、定義によって制御することができます。

マクロと特殊形式は、どのように引数の評価をするかを制御できるという点ではまったく同じです。 違いは、システムのどのレベルで定義されているかという部分です。 Cで書かれたLisp処理系があったとして、特殊形式はCで書かれていて、マクロはその言語(Lisp)で書かれています。 そして、普通の利用のしかたでは、マクロを自分で書くことはできますが、特殊形式はできません。


さて、先に出た、「(def a 42)」という式のdefは特殊形式なので、これ評価した場合、

defという特殊形式に、「a」と「42」が渡され、defの定義にしたがって処理される。

ということになります。そして、defの定義は、

「第一引数は評価せず、それに第二引数を評価した値を束縛する」

です。よって、「a」と「42」が渡された場合、「a」に、「42」を評価した42が束縛されるということになります。

user> (def a 42)
#'user/a
user> a
42


defもシンボルです、defを評価してみましょう。

user> def
CompilerException java.lang.RuntimeException: Unable to resolve symbol: def in this context, compiling:(NO_SOURCE_PATH:1:1208)


エラーになりました。 「Unable to resolve symbol」ということは、defには何も束縛されていないということです。 これは、defが特殊形式だからです。

andはマクロです。定義は、与えられた引数を順に評価して、返り値がtrue以外の物ががあればその返り値を返し以降の引数は評価しません。すべてtrueであればtrueを返します。

user> (and (> 10 3) (< 3 5))
true
user> (and (> 10 3) (< 3 5) (= 3 4) (> 5 2))
false

andを評価してみましょう。

user> and
CompilerException java.lang.RuntimeException: Can't take value of a macro: #'clojure.core/and, compiling:(NO_SOURCE_PATH:1:1208)

やはりエラーになりました。しかし、特殊形式とは異なります。マクロは表示形式を持っていないので印字できないということのようです。

マクロなの?関数なの?

使っているものがマクロや特殊形式なのか関数なのかは、パッと見ただけではわかりません。そうれだと、引数が評価されるかいちいち確認しないとわからなくて面倒だという話を聞きます。

僕はそれをあまり気にしたことがありません。
なぜなのかなぁと思って考えてみると、評価されるかどうかはあまり問題になる場面が無いからなのではないかと思います。

たとえば、ifはマクロです。

(if true
    (+ 3 5)
    (-  3 5))

という式では、(- 3 5)は評価されません。 ですが、評価されても問題ありませんよね? これは、関数型の作りになっているからです。
評価されると困るということは、
  - 副作用があるから、評価されるかどうかによって結果が異なる。
  - 処理にとても時間がかかるから、不要な場合は評価したくない。
のどちらかでしょう。少なくともclojureではどちらもそれほど多くないでしょうから、そのときはどのように評価されるのか調べれば済むわけです。

クオート

見てきたように、Lispは基本的にすべてのものを評価します。ですが、評価したくない場合もあります。このような場合に、その評価を1回なかったことにするクオートという機能を持っています。何かをクオートするには、「'」記号を前に付けます。

よく、「クオートされたものは評価されない」という説明がされていることがありますが、これ、違います。

クオートされたものを評価すると、そのものが返る

です。

user> 'a
a

「a」をクオートしたものを「評価する」と「a」が返ります。

どんな時にクオートを使うのでしょう?
よくクオートを使うのはやはりリストでしょう。Lispのリストは見てきたように関数呼び出しなのですが、リストはデータ構造としても利用します。

3教科の点数のリストは、(72 55 96) などと表わせます。これをeに束縛したいときに、

user> (def e (72 55 96))

などとすると、

ClassCastException java.lang.Long cannot be cast to clojure.lang.IFn  clojure.lang.Compiler$InvokeExpr.eval (Compiler.java:3454)


と怒られます。 「java.lang.Long cannot be cast to clojure.lang.IFn」となっていますね。defの動作は、「第一引数は評価せず、それに第二引数を評価した値を束縛する」です。 「e」は評価されませんが、第二引数の「(72 55 96)」は評価されてしまうのです。
ためしに(72 55 96)をそのまま評価すると、同じエラーが返ります。

user> (72 55 96)
ClassCastException java.lang.Long cannot be cast to clojure.lang.IFn  user/eval9466 (NO_SOURCE_FILE:1)

これはリストですから、一番目の要素を評価してそれを関数として処理しようとします。 72を評価すると72が出ます。72にのこりの引数の評価結果を渡して処理させようとするのですが、これは「java.lang.Long」なので、関数でもマクロでも特殊形式でもないので、エラーとなったわけです。


でも、ここでやりたいのは、(72 55 96) をリストとしてeに入れることです。そんなときにはクオートです。

user> '(72 55 96)
(72 55 96)

このように、クオートしたものを評価するとそのものが返ります。なので、「e」にこのリストを束縛したいときには、こうします。

user> (def e '(72 55 96))
#'user/e
user> e
(72 55 96)

これで、「e」に、第二引数の「'(72 55 96)」を評価した(72 55 96)を束縛することができました。


Clojureにはリストの他に、ベクタやマップ・セットなどのリテラル表現があります。これらはリストとは違って、関数呼び出しとはみなされません。ですが、各要素が評価されるという点では同じです。

user> [1 2 3]
[1 2 3]

ベクタはベクタに評価されました。ですが、評価が行なわれないわけではありません。リストと同様にそれぞれの要素はすべて評価されるのです。なので、a,b,cに何も束縛されていない場合、以下のものはエラーになります。

user> [a b c]
CompilerException java.lang.RuntimeException: Unable to resolve symbol: a in this context, compiling:(NO_SOURCE_PATH:1:1208)


「a」が評価できないからです(bもcもですが)。

user> a
CompilerException java.lang.RuntimeException: Unable to resolve symbol: a in this context, compiling:(NO_SOURCE_PATH:1:1208)


シンボルに値が束縛されていればOKです。

user> [x y z]
[3 10 42]


シンボルのベクタを作りたいときにはどうすればいいでしょう?
リストのときのように、クオートすればOKです。

user> '[a b c]
[a b c]

ベクタの場合、それぞれの要素をクオートしてもOKです。

user> ['a 'b 'c]
[a b c]

リストの場合はこれはだめです。

user> ('a 'b 'c)
c

リストなので、関数呼び出しになってしまいます。

※なぜこれが「c」になるかというと、Clojureではシンボルが関数として呼ばれると、第一引数をマップとして扱って、そのシンボルをキーとして持つ値を返すのですが、そのキーが無い場合は、第二引数を返すからです。


eval


クオートすると、評価を1回遅らせることができました。
逆にさらに評価することができると便利な場面があります。それをするのが「eval」です。

user> (+ 1 2)
3

user> '(+ 1 2)
(+ 1 2)

user> (eval '(+ 1 2))
3

evalはS式を受け取るとそのS式を評価して結果を返します。REPLの「E」はevalのEです。
evalを使うと、プログラムでS式を組み立てて、それを評価することができます。プログラムを作るプログラムです。
このあたりが、Lispが最強であると言われるゆえんなのですが、この目的であれば、同様の機能をもつマクロを使うのが一般的なので、evalの出番はあまり無いかもしれません。

まとめ


Lispのシンボルの評価については、「どうなっているんだかよくわからない」という評価をよく耳にしますが、見てきたように、難しいことは無くて、基本的にすべての物が評価されるのです。そして、マクロやクオートやevalを使うことで、評価方法をコントロールすることもできます。
また、リストは関数呼び出しとして使われると同時に、データ構造としても使われるので、まぎらわしいところはあると思います。Clojureではその点はベクタやマップ・セットなど独自のリテラル表現を用意することで、かなり低減しているのではないかと思います。

なんかちっともまとまってませんがこれで。



0 コメント:

コメントを投稿