Clojure言語基本の「き」

ふと思い立ってClojureの勉強を始めることにしました。 - 達人プログラマーを目指してで書いたように、10日前にClojureの勉強を始めました。まだ勉強を始めたばかりのということもあり、他人に上手く正確に説明できる段階ではないかと思いますが、自分自身超初心者の立場で書くということも有意義であると思うので、間違いを恐れずに今まで理解したことのポイントについて書いてみようと思います。*1

スカラー値のリテラル

ClojureはREPL(Read Eval Print Loop)という対話的実行環境を持つので、いきなり大きなプログラムを作成しなくても、一行一行結果を確認しながら勉強することができます。したがって、数値や文字列などのリテラルをREPLのプロンプトに入力すると、値がそのまま結果として返ってくることが確認できます。

user=> 0xff
255
user=> 0377
255
user=> 1.1
1.1
user=> 1.5e10
1.5E10

以上のように数値のリテラルの書き方はだいたいJavaと似ています。ちょっと特徴的なのは整数の基数を示すrという記法が使えることで、36進数まで表現できます。

user=> 2r11111111
255

文字列リテラルJavaとよく似ているのですが、中に改行を埋め込むことができます。

user=> "Hello World"
"Hello World"
user=> "こんにちは 世界"
"こんにちは 世界"
user=> "Hello
World"
"Hello\nWorld"

文字リテラルの書き方はJavaと違っていてシングルクオーテーションで囲む代わりにバックスラッシュ(\記号)の後に文字を書きます。(Javaのように改行文字などを表現するにはユニコードで指定する必要があります。)

user=> \a
\a
user=> \n
\n
user=> \u0042
\B
user=> \u30DE
\マ

さらに、ClojureにはJavaのnullに相当するnilやtrue、falseといったリテラルが使えます。

関数の呼び出し

単に入力した値をそのまま表示するだけではつまらないので、簡単な計算をしてみたいと思います。

user=> 1 + 1
1
#<core$_PLUS_ clojure.core$_PLUS_@15353154>
1

意図に反して2が表示されずに、変な記号が表示されました。(これは各オペランドをそれぞれ独立して評価した結果となっています。)ここが、Lispに慣れていないと最初に敷居が高いところだと感じたのですが、足し算など四則演算も含めてClojureでは関数呼び出しの形式で記述する必要があります。つまり、「+」という名前の関数を呼び出すと考えるわけですね。算術演算を含めて常に前置記法で書く必要があるのです。

user=> (+ 1 1)
2
user=> (* 3 5)
15

一般に丸かっこはリストと呼ばれていますが、(関数名 param1 param2 ... paramN)の書き方で関数を呼び出すことができます。
もちろん、この呼び出しは入れ子にすることもできます。例えば、(1+2)*3を表現するには、以下のように記述します。

user=> (* (+ 1 2) 3)
9

慣れないと、相当奇妙な感じですが、この書き方にはちょっと便利なところもあって自然に可変長パラメータが実現できるのですね。だから、3つ以上の足し算も同様に記述できます。

user=> (+ 1 2 3 4 5)
15

しかも、四則演算だけでなく他の関数呼び出しもいつも同じ書き方になるという美しさもあります。他の言語のように演算子の結合規則や優先順位といったものを覚える必要がありません。たとえば、Javaで+演算子を使って行うような文字列結合をするにはstr関数を使えますが、この場合も同じ記法で呼び出すことができます。

user=> (str "Hello" " " "World")
"Hello World"

defを使った変数の定義

四則演算と文字列結合のやり方がわかったら、プログラマーが普通次に考えることは変数の定義と値の代入でしょう。それで、厳密に言うと値の入れ物としての変数に直接相当するものはないようで、本には最初から名前空間とかvarの束縛といった難しい説明がされているのですが、ここでは詳細は考えないことにして、とにかくdef特殊形式*2を使って変数の定義と値の代入に相当することを行うことができます。

user=> (def a 100)
#'user/a
user=> (def b 200)
#'user/b
user=> (+ a b)
300

正確な数値演算

Clojureのちょっと面白い特徴として、JavaではBigIntegerやBigDecimalなどを使って行う計算を容易に実行できるということがあります。さらに、精度は必要に応じて自動的に変換されるため、メモリが許す限りどんなに大きな数でも計算できます。

user=> (class (* 1000 1000 1000))
java.lang.Integer
user=> (class (* 1000 1000 1000 1000))
java.lang.Long
user=> (class (* 1000 1000 1000 1000 1000))
java.lang.Long
user=> (class (* 1000 1000 1000 1000 1000 1000))
java.lang.Long
user=> (class (* 1000 1000 1000 1000 1000 1000 1000))
java.math.BigInteger

ここではclass関数を使って実際の数値の型を調べていますが、必要な桁数に応じて実際の数値のオブジェクト型が適切に変換されていることがわかります。(数値や文字列の型がClojure独自でなくてJavaのクラスになっているところも興味深いところです。)実数*3の計算については、何も指定しないとJavaのdoubleとして計算されます。だから、桁落ちが発生します。以下の例では、計算の順序によって計算結果が異なり、通常の足し算の結合法則が成り立たないことがわかります。なお、以下のようにセミコロン以降がコメント文として無視されます。

user=> (def a 1.0e50)
#'user/a
user=> (def b -1.0e50)
#'user/b
user=> (def c 17.0)
#'user/c
user=> (+ (+ a b) c) ; (a + b) + c 
17.0
user=> (+ a (+ b c)) ; a + (b + c)
0.0

この場合、数値の最後にMという記号を付けることで値を正確に計算できるようになります。

user=> (def a 1.0e50M)
#'user/a
user=> (def b -1.0e50M)
#'user/b
user=> (def c 17.0M)
#'user/c
user=> (+ (+ a b) c)
17.0M
user=> (+ a (+ b c))
17.0M

正確な演算は実数だけではありません。割り算について、割り切れない場合は実数として処理されずに、分数として扱われるため、有理数を正確に表現することができます。以下のように通分などの計算もできます。

user=> (def a (/ 1 2))
#'user/a
user=> (def b (/ 1 3))
#'user/b
user=> (+ a b)
5/6

分数の計算は変数を使わなくてもできます。

user=> (+ 1/2 1/3)
5/6

なお、これらの結果得られた分数を実数に変換するためには実数と計算するかdoubleにキャストします。

user=> (+ 5/6 0.0)
0.8333333333333333
user=> (double 5/6)
0.8333333333333333

コレクションのリテラル

今まで使用してきた関数呼び出しは括弧に複数の要素が記述された形になっており、リストと呼ばれるもっとも重要なコレクションです。Clojureにはリストの他にもいくつかの種類のコレクションを利用することができます。*4

ベクタ

普通の丸括弧の代わりに角括弧で囲むことでベクタを記述することができます。ベクタはJavaの配列やListに近い概念で、要素が順序づけされたデータ構造を表します。

user=> [1 2 3]
[1 2 3]
user=> [1 2 "Hello"]
[1 2 "Hello"]

ベクタには様々な型の要素を入れられます。なお、Clojureではカンマ「,」は空白やスペースと同じように無視される文字なので、お好みで以下のように記述することも可能です。こちらの書き方のほうがGroovyやJava Scriptのようですね。

user=> [1, 2, "Hello"]
[1 2 "Hello"]

ベクタの要素をインデックスで一つ一つアクセスするのは、あまりClojure的ではないと思うのですが、以下のようにしてget関数かnth関数を使って行えます。*5

user=> (def a [1, 2, "Hello"])
#'user/a
user=> (get a 2)
"Hello"
user=> (nth a 2)
"Hello"

さらに、実はベクタそのものを関数のように記述することもできます。

user=> (a 2)
"Hello"
マップ*6

マップは中括弧を利用して{キー 値 ...}の形で記述できます。ここでも適当にカンマを利用して要素を区切ることができます。

{1 "one", 2 "two", 3 "three"}

なお、マップのキーとしてはキーワードと呼ばれる特殊な値を利用することが一般的です。これはややこしいのですが、「:」が先頭に来る書き方も含めてRubyのシンボルに近いものです。(Clojureのシンボルはdefなどで定義した変数名や関数名のこと)

{:key1 "one", :key2 "two", :key3 "three"}

マップの要素もget関数でアクセスできます。

user=> (def a {:key1 "one", :key2 "two", :key3 "three"})
#'user/a
user=> (get a :key1)
"one"
user=> (get a :key2)
"two"

ちょっと面白いのは、シンボルそのものを関数のようにして呼び出すことが可能ということです。以下の書き方でもシンボルをキーとする値を取得できます。

user=> (:key3 a)
"three"
セット(集合)

以下の記述方法でセットを記述できます。中括弧の前にシャープ記号*7を書きます。

#{1 2 "Hello"}

関数の定義方法

最後に独自の関数の定義方法について説明します。以上説明してきた基本が理解できていれば、関数の定義方法を理解することは容易です。実際、関数の定義は以下の形式でdefnマクロを使って行うことができます。

(defn 関数名
"ドキュメントコメント" ; 省略可能
[p1 p2 ...] ;パラメーター群を表すベクタ
関数本体)

これは同図像性と呼ばれるそうですが、こうした関数定義そのものがデータ構造と同じ構文で表現できる、つまりコードがデータで組み立てられているというのがClojureの特徴の一つのようです。実際、名前を受けとってメッセージ文字列を作成して返す関数は以下のように定義できます。

user=> (defn greeting
"名前を受け取ってメッセージを返す。"
[name]
(str "Hello, " name))
#'user/greeting

そうすると、呼び出しは標準の関数と同様に以下のようにして行えます。

user=> (greeting "Ryo")
"Hello, Ryo"

なお、省略可能なドキュメントコメントを定義している場合はdoc関数を使ってドキュメントを表示できます。

user=> (doc greeting)
-------------------------
user/greeting
([name])
  名前を受け取ってメッセージを返す。
nil

もちろん、Java Scriptと同様にClojureでは関数は第一級市民の値なので、関数のパラメータに簡単に関数自身を引き渡して呼び出すことができます。

user=> (defn calc-print 
  [a b f]
  (str "Answer = " (f a b)))

#'user/calc-print
user=> user=> (calc-print 1 2 +)
"Answer = 3"
user=> (calc-print 1 2 -)
"Answer = -1"

まとめ

今回は、超初心者の立場で、厳密性は犠牲にしつつもClojureの基本的な使い方について説明しました。構文が一般的な*8プログラミング言語と大きく異なるため、最初は見た目がすごく難しそうなのですが、実は言語の文法自体は驚くほどシンプルなのであり、基本を理解すれば意外にわかりやすいと感じました。
とは言え、本の先のほうを見てみると以下のような話題が書かれていて、まだまだ自分で有用なプログラムを書けるようになるにはしばらく時間がかかると思いますが、少しずつ勉強していきたいと考えています。

これらについては、また続編で説明できればと思います。

*1:そういう事情なので、内容の正確さについては全く保障の限りではありませんが、間違っている点があればご指摘いただければ幸いです。

*2:見かけ上defも関数呼び出しの形に見えるので、最初はそんな理解でもよいのかと思いますが、厳密には処理されるタイミングがコンパイル時となるため、関数ではありません。

*3:ここで実数という用語は数学の用語というよりプログラミングの分野での方言ですね。

*4:さまざまなコレクションがありますが、これらはシーケンスとして抽象的に考えることで、多くの場合同じように使用することができます。

*5:getとnthは存在しないインデックスの時に異なります。getはnilが返るのに対して、nthの場合は例外となります。

*6:Clojureにはmap関数というのがあってややこしいのですが、こちらは普通の連想配列としてのマップです。

*7:このシャープ記号はリーダーマクロを起動する文字の一つのようです。

*8:ALGOL系言語の