clojure備忘録[前提としてのLisp]

replでいろいろ遊ぶ前に、Lispの基本的な概念についてもざっくりとですがまとめておきます。


Lispは、他の一般的な手続き型の言語と比べて独特の性質があります。
clojureLispの方言の一つですから、その独特さを理解するためには、
どうしても基本的なLispの知識が必要となってしまいます。


今回はclojureの理解を助ける為に私の主観で最低限必要だと思う、Lispのエッセンスについて説明してみます。
力不足な感は否めませんがご容赦を。

「リスト」と「アトム」

Lispには根源的な2種類のデータがあります。(データ「型」とはちょっと違うような気がします。。)
一つは「リスト」、もう一つは「アトム」です。
従来のLispではこれらのデータに対する正しい定義がありますが、clojureとの兼ね合いの為に今から嘘を書きます。
正しい定義を知りたい場合は、Wikipediaで「純LISP」を調べて見てください。
(ここに記載されているリストの表記をclojureで行うと、正しい操作ができないのです。。schemeというLisp方言ならこの表記で正しく動作してくれます。)


clojureを視野に入れた上での「リスト」の定義は、


リスト、またはアトムを線形に組み合わせたもの
であり、括弧を使って次のように記述します。

()
(a)
(a b)
(a b ...)

一番上は括弧の中に何もありません。これを特に「空リスト」と言います。
上述の「リスト」の定義では、リスト自身もリストの中に組み合わせることができることになりますので、次のような表現もやはりリストと言えます。

(a (b c))
((a b) (c d) e)

リストの入れ子を許可することにより、あらゆるデータを構造化することが可能になります。
この「あらゆるデータを構造化できる」というのはLispに非常に大きな力を与えることになります。


次に「アトム」の定義ですが、「リスト以外のもの」となります。
なんだそりゃ、という感じがしますが、アトムは括弧で自分を包む必要がなく、それ一つだけで何かを表現するデータだと思えばよろしいかと。
具体的には「論理値(true/false)」「数値」「文字」「文字列」「nil」等が該当します。他にもあるかも。


S式

「S式」はLispの力の根源です。
Lispのコードは全て「S式」で構成されています。


とは言っても「S式」は大仰なものではなく、
正体は、実は上で説明した「リスト」と「アトム」です。

つまり、
Lispのコードは全てS式で構成されている」ので、
Lispのコードは全てリストとアトムで構成されている」ことになり、それはつまり、
Lispのコードは全てLispのデータである」と言えるわけです。
これが力の根源のカラクリなのですが、まだピンとこないと思います。
後々明らかにしていきます。


さて、「コード」である以上は複雑な処理を記述するわけで、
その複雑な処理をアトム一つだけで記述できるわけがありません。
当然ながら、リストの形式でLispコードは書き下されます。
次のコードは、schemeというLisp方言で記述した場合の関数の定義になります。

(define (hello name) (string-append "Hello " name "! Welcome to the Lisp world!!"))

ダブルクオートで囲まれた要素は、文字列のアトムになります。
上記関数定義を評価させる時のコードはこんな感じになります。

(hello "awacio")

評価結果は、次のようになります。

"Hello awacio! Welcome to the Lisp world!!"

関数定義はリストの入れ子、評価式もリスト、評価結果は文字列のアトム、
というように、ぜ〜んぶS式でになっていますね。

式指向の言語

この事から、「Lispは式指向の言語である」とも言えます。
通常の手続き型言語は、「式」と「文」が分けられているケースが多いです。
この「式」と「文」にはそれぞれ次のような特徴があります。

「式」値を返すもの
「文」値を返さないもの

例えばC言語であれば、if文というのがあります。

int c = 0;
if (a == b)
    c = 100;
else
    c = 200;

このif文は、値を返せません。
この制約のせいで、真、偽、両方のケースで、それぞれcに適切な値を代入するしかないわけです。
が、一方でC言語には三項演算子というものがあり、これを利用すると、より簡潔にcに適切な値を代入することができます。

int c = a == b ? 100 : 200

「=」の右辺は値を返すので「式」であると言えるわけです。


「式」であれば更に他の「式」と組み合わせることが可能になる為、
次のような更に複雑な計算をさせた値をcに代入させるようなマネも可能です。

int c = ((a == b ? 100 : 200) * (year < 1989 ? 1.0 : (year < 1997 ? 1.03 : (year < 2014 ? 1.05 : (year < 2015 ? 1.08 : 1.10)))))

(無駄に横に長いですね。)
くどいようですが、if文を使った場合は同じことはできません。
「文」は値を返さないが故に、組み合わせることができないからです。


ここで説明している内容の狙いは、「見やすいかどうか」という点ではありません。
「記述能力の柔軟さ」に注目した場合、「文」よりも「式」の方が優れている、という事を認識してもらうのが狙いです。
ちなみに、clojureで上記の複雑な式を表現するとこのようになります。

(def c (* (if (= a b) 100 200)
          (cond (< year 1989) 1.00
                (< year 1997) 1.03
                (< year 2014) 1.05
                (< year 2015) 1.08
                :else         1.10)))

ここではこのコードに関する詳細な説明はしませんが、
Lispコードは全てが「式」なので、
「文」が持つ制約から完全に解放されていること、
故に、文法的にはコードの記述場所や組み合わせ方に制限がないこと、
これらが感触として伝わればと思います。


評価順序

LispのコードがS式だということは理解して頂けたと思います。
上述した通り、コードは大概リストの形式でこのように書き下されます。

(arg0 arg1 arg2 ...)

このリスト中の先頭の要素arg0が何者であるかによって、引数の評価順序が変わります。
以降で順に説明しますが、arg0は「関数」か「特殊形式」か「マクロ」でなければなりません。
非常に薄っぺらいですが、それぞれについて説明していきます。


関数

もっとも良く使用するので、まずはこれから。
関数はカスタム処理を定義する為に存在します。
組み込みの関数もありますし、
ユーザが自分で定義を記述して読み込ませ、呼び出すこともできます。


さて、arg0が関数であった場合、

(arg0 arg1 arg2 ...)

というリストはどう評価されていくかというと、


1.引数のarg1、arg2、...のS式が先に評価される。
2.引数が全て評価され終わった後、arg0の関数に各引数の評価結果が渡され、arg0で定義されている関数が評価される。

という順序になります。
もう少し具体的に説明してみます。次のような条件を満たす関数を定義したとします。

名前func0
引数a, b
内容a と b を加算した結果を返却する。

この関数を使って、次のようなS式を評価したとします。

(func0 (+ 2 3) (- 5 3))

これは次のような順序で評価が進行していきます。

(func0 (+ 2 3) (- 5 3))
       ↓      ↓
(func0 5       2      )(+ 5 2)7

う〜む、我ながらわかりにくいなぁ。。
まずfunc0の引数のS式である「(+ 2 3)」と「(- 5 3)」が先に評価され、
それぞれ「5」「2」になります。
それがfunc0に渡されて「(+ 5 2)」というS式に変換され、
これが更に評価されて最終的に「7」というS式になるわけです。


「そんなの当たり前じゃん」と思われるかもしれませんが、
そうでないのが次の「特殊形式」です。


特殊形式

他の言語では大抵「構文」として提供されている機能に相当するのがこの「特殊形式」になります。
前述の「式指向の言語」の項目で記載したif文の「分岐」はまさにこれに相当します。
Lispではこれらの制御も「式」として記述します。
「式」で記述することにより、関数呼び出しと変わらない見た目、構造で記述することが可能になります。


では関数呼び出しとは何が異なるのか、という話になりますが、
特殊形式の場合、引数が「遅延評価」されるという点が大きな違いとなります。
ここでは分岐制御の「if」を題材にし、具体例から説明を試みてみます。

次のコードを脳内でシュミレーションしてみましょう。

;変数 a に 2 を設定
(def a 2)

;ifを使用して分岐してみる。
(if (= a 0) nil (/ 10 a))

上記のifが関数だったとしたら、前述の関数の項目で説明したように引数は先行評価されます。
その結果どのようになるでしょうか。

(if (= a 0) nil (/ 10 a))

(if (= 2 0) nil (/ 10 2))
    ↓          ↓
(if false   nil 5       )5

うまく動いているように見えます。
では、変数 a の値が 0 だったら?

(if (= a 0) nil (/ 10 a))

(if (= 0 0) nil (/ 10 0))
    ↓          ↓
(if true    nil [×]    )


×の部分でゼロ除算が発生してしまっています。
我々が「if」という制御に期待しているのはこのような動作ではありません。
条件式の部分(第1引数)が真の場合は、
第2引数の結果を返却し、第3引数である除算は評価してほしくなかったはずです。
が、「関数」である以上、引数は全て一斉に先行評価されてしまいます。
それ故に、期待した結果ではなく、エラーが発生してしまっているわけです。


では本来のifの評価順序はどうかというと、このような感じになっています。

;a が 2 の場合
(if (= a 0) nil (/ 10 a)) ;条件のS式のみが評価され・・・(if (= 2 0) nil (/ 10 a)) ;第2、第3引数は評価されていない。(第2引数は見た目分からんけども。)(if false   nil (/ 10 a)) ;条件のS式がfalseならば・・・(/ 10 a)  ;第2引数のnilはn完全に無視し、ここで始めて第3引数の評価に入る。(/ 10 2)  ;第3引数の変数 a に 2 を設定し・・・5         ;評価を行って 5 を得る。

;a が 0 の場合
(if (= a 0) nil (/ 10 a)) ;条件のS式のみが評価され・・・(if (= 0 0) nil (/ 10 a)) ;第2、第3引数は評価されていない。(第2引数はやはり見た目分からんけども。)(if true    nil (/ 10 a)) ;条件のS式がtrueならば・・・nil           ;第3引数の除算のS式は完全に無視され、第2引数の nil が返却される。


このように、特殊形式は自身の引数に対して「評価の順序」や、
「評価するかしないか」等を内部で制御しているというのが関数と異なる特徴になります。
当然ながら、これらの制御の組み合わせは、各々の特殊形式において全て異なります。
良く使用される特殊形式については後のエントリで紹介します。

マクロ

前項の「特殊形式」の説明の中で「遅延評価」という単語が出てきました。
この遅延評価の機能を存分に利用しているのが次に説明するマクロになります。
とはいえ、実は私まだマクロを使いこなせていないので、ごくごく簡単に説明を。


マクロは「プログラム生成」の為に存在しています。
マクロの呼び出しもS式になります。

(macro arg1 arg2 ...)

特殊形式と同じく、引数のarg1、arg2、...は遅延評価されます。
すなわち、その字面のS式がそのままデータとしてmacroに渡されます。
マクロは、引数として受け取ったS式をデータとして受け取り、
必要に応じてそのデータを分解し、
プログラマが意図したコードに再構築します。
こんなマネができるのは、コードの全てがS式で構築されるからに他なりません。
再構築されたプログラムもやはりS式になります。

このような挙動が可能になると、
少ないコードからパターン化した巨大なコードを自動生成したり、
新しいフロー制御を構築することも可能になります。
本エントリのような簡単な説明の時にはマクロの例を挙げにくいのですが、一応試みてみます。


まずはjavaで導入されたfor each構文です。

int[] data = new int[] { 1, 2, 3, 4, 5 };

for (int value : data) {
    System.out.println(String.format("%d ^ 2 = %d\n", value, value * value));
}

for文に、インデックス不要の構文が存在します。渡されたdataは配列で、
繰り替えされる度に配列の要素を順にvalueに格納し、
forブロック内のロジックが実行されます。便利ですね。


さて、clojureにはこの手の特殊形式はありません。
というか、繰り返し制御は「末尾再起の最適化」でもって行います。
ここでは「末尾再帰の最適化」は詳しく説明しませんが、
loopで再帰処理を開始し、終了条件になるまでrecurを実行したらloopを再度呼び出す動きをすると思ってください。
ではclojureのコードを掲載します。

(def data [1 2 3 4 5 6])

(loop [value     (first data)
       rest-data (rest data)]
  (if (= value nil)
      nil
      (do
        (println (format "%d ^ 2 = %d" value (* value value)))  ;←★
        (recur (first rest-data) (rest rest-data)))))

(※firstは引数のリストの先頭要素を取得する関数、restは先頭以外の残りの要素をリストとして取得する関数になります。)


めちゃくちゃ泥臭いですね。
ループ処理の枠組みはしょっちゅう使いたいですが、実際にカスタムしたい処理は★の行だけです。
ここだけを変えた処理を実装したいのに、毎回こんなに大量に、しかもコピペに近い感じでコードを記述するのはまったくもってナンセンスです。


というわけで、foreachマクロを作ってみましょう。(「for」は「リスト内包表記」の為に既に予約されていますのでforeachになります。)
マクロコードそのものの詳細説明は省きます。(後でマクロのエントリを作り、そちらで詳しく説明してみます。)

(defmacro foreach
  [bindings & body]
  (let [item  (first bindings)
        items (second bindings)]
    `(loop [~item  (first ~items)
            items# (rest ~items)]
       (if (= nil ~item)
           nil
           (do
             ~@body
             (recur (first items#) (rest items#)))))))


replに読み込ませてみてください。


user=> (defmacro foreach
[bindings & body]
(let [item (first bindings)
items (second bindings)]
`(loop [~item (first ~items)
items# (rest ~items)]
(if (= nil ~item)
nil
(do
~@body
(recur (first items#) (rest items#)))))))
#'user/foreach
user=>


早速今定義したforeachマクロを使ってみましょう。

(def data [1 2 3 4 5 6])

(foreach (value data)
  (println (format "%d ^ 2 = %d" value (* value value))))

上記コードをreplに評価させてみてください。


user=> (def data [1 2 3 4 5 6])
#'user/data
user=> (foreach (value data)
(println (format "%d ^ 2 = %d" value (* value value))))
1 ^ 2 = 1
2 ^ 2 = 4
3 ^ 2 = 9
4 ^ 2 = 16
5 ^ 2 = 25
6 ^ 2 = 36
nil
user=>

このように、マクロを使ってforeach構文もどきの制御式を実装することができてしまいました。
これは紛れもなく、他の言語で言うところの「文法」を追加した事に相当します。
通常の言語の場合、文法や構文を言語に追加したければ、言語処理系の字句解析器、構文解析器、実行器(または評価器)に直接手を入れるしかありません。
ところが、今回clojureで追加したforeach制御式は、あくまでもclojureの言語機能の範疇で追加したに過ぎません。
JVMにも、clojure.jarにも手を入れてませんよね?

これがLispLisp以外の言語の力の違いになります。


但し、サンプルを見ると分かるように、マクロ定義の記述は分かりにくいです。
あまり乱用はしないように、「マクロクラブのルール」というのがあるようです。


ルール1.マクロは使うな。
ルール2.それがパターンをカプセル化する唯一の方法ならばマクロを書け。
<例外>.同等の関数に比べて呼び出し側が楽になるならばマクロを書いても構わない

今は何の事だか、私もイメージできませんが(ヲイ)心に止めておきましょうw