読者です 読者をやめる 読者になる 読者になる

一時間で覚えるClojure

一時間で覚える Ruby
を真似して書いてみました(この文章はあまり役に立ちません)。

インストール

ここから、
最新版(2010/05/10現在 ver.1.1.0)のzipをダウンロードして解凍します。

Javaランタイム
も必要かもしれません。普通のPCなら入っていると思いますが。

取り合えず動かしてみる。

シェル(またはコマンドプロンプト)で、解凍して出来たフォルダの中で、

> java -cp clojure.jar clojure.main

と打ち込むと対話環境(REPL)に入れます。

Clojure 1.1.0
user=>

伝統に従って、世界に挨拶してみましょう。
'(println "Hello World!")'と入力し、Enterキーを押します。
このとき、カッコは省略できませんので、注意。

user=> (println "Hello World!")
Hello World!
nil

出力にはprintln と printを使います。
printlnは勝手に後ろに改行を追加し、printは改行を追加しません。

nilは (println "Hello World!")の値です。

足し算とかも試してみましょう。

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

ビックリさせてすみません。Clojureでは、+ や *などの演算子を数字の前に書きます。
この記法(S式)のメリットは、演算の優先順位を気にする必要がなくなる事と、
('1 + 2 * 3' という記法には『掛け算は足し算より先に計算する』というルールが必要です。)
マクロが使えることです。

なお、ファイルに書き込んだプログラムを実行するには、

> java -cp clojure.jar clojure.main script.clj

と、最後にファイル名を渡せばよいです。

いくつかのデータ型とそのメソッド (10 分)

データの型はclass 関数で調べられます。

user=> (class 1)
java.lang.Integer
user=> (class "spam")
java.lang.String

Clojureでは、かなりの型をJavaから借りて来ています。
Clojureの文字列と文字は、Javaの文字列・文字そのものです。
また、文字列の操作も、多くの場合Javaメソッドをそのまま使います。
Javaメソッドは、メソッド名に'.'を付けた関数をとして呼び出せます。

user=> (.concat "spam" "egg")
"spamegg"
user=> (.indexOf "spam" "p")
1
user=> (get "spam" 0)
\s
||


値の並びをあらわすには、vectorを使います。添え字は0から始まります。
>|clojure|
user=> [1 2 7 19]
[1 2 7 19]
user=> (get [1 2 7 19] 2)
7

どんな値も、strで文字列に変換できます。

user=> (str [1 2 7 19])
"[1 2 7 19]"
user=> (str 42)
"42"

関数の作成

関数はdefnで定義できます。

user=> (defn add [x y]
         (+ x y))
#'user/add
user=> (defn say-hello []
         (print "hello"))
#'user/say-hello

最後に評価した値が、関数の値になります。

作った関数は元からある関数と同様に呼び出せます。

user=> (add 1 2)
3
user=> (say-hello)
hellonil

また、fnや#で無名関数を定義できます。

user=> (#(+ %1 %2) 100 53)
153
user=> ((fn [x y] (+ x y)) 100 53)
153

制御構造(は、厳密には無い)

ifは条件が論理的に真なら最初の引数を、偽なら2番目の値を返します。

user=> (defn less-than-100? [x]
         (if (< x 100) "yes" "no"))
#'user/less-than-100?
user=> (less-than-100? 50)
"yes"
user=> (less-than-100? 500)
"no"

なお、偽の場合の値は省略することも出来て、その場合はnilが返ります。
真偽の判断では、falseとnil以外は全てtrueとみなされます。
0も空文字列もtrueであることに注意してください。

ifでは、分岐先に一つの値しか書けません。
分岐後に複数のアクションを起こしたいときは、doを使います。
doは全ての引数を評価し、最後の値を返します。

user=> (defn less-than-100? [x]
         (if (< x 100) 
             "yes"
             (do
               (println "Big Number" x)
               "no")))
#'user/less-than-100?
user=>  (less-than-100? 500)
Big Number 500

doは副作用を扱うときに使用します。
というのも、doは最後の値以外は全て捨てるので、
途中の値が意味を成すには副作用を持つしかないのです。


mapはシーケンスの各要素に、関数を適用したものを返します。

user=> (defn add2 [x] (+ x 2))
#'user/add2
user=> (map add2 [1 2 3 9])
(3 4 5 11)

filterはシーケンスの各要素で、条件を満たすもの集めて返します。

user=> (filter even? [1 2 3 4])
(2 4)

mapとfilterを同時に使うかわりに、リスト内包表記を使うことも出来ます。
リスト内包表記は条件式が複雑な場合やネストした場合に便利です。

user=> (map add2 (filter even? [1 2 3 4]))
(4 6)
user=> (for [x [1 2 3 4] :when (even? x)] (add2 x))
(4 6)
user=> (for [x [1 2 3 4] :when (even? (- x 1))
             y [9 8 7 6] :when (odd? (- y 2))]
         (add2 (+ x y)))
(13 11 15 13)

loopとrecurはループをするのに使います。

user=> (loop [sum 0 x 5]
         (if (= 0 x)
           sum
           (recur (+ sum x) (- x 1))))
15

最初、loopはsumを0、xを5に束縛します。
xの値5は0ではないので、loopの先頭に戻ります。
このとき、sumは(+ sum 5) つまり 5 に、x は (- x 1) つまり 4 に束縛しなおされます。

xの値4は0ではないので、loopの先頭に戻ります。
このとき、sumは(+ sum 5) つまり 9 に、x は (- x 1) つまり 3 に束縛しなおされます。

これを繰り返し、x が 0になった時、sumの値がloop全体の値とて返されます。
loop全体の値は 5 + 4 + 3 + 2 + 1 = 15です。


シーケンスの中身に対して繰り返す時はdoseqを使えます。

user=> (doseq [x [0 1 2 3 4]]
         (println x))
0
1
2
3
4
nil

変数

letは値に名前を付けるのに使います。
付けた名前はletの中だけで有効です。

user=> (defn rect [cx cy w h]
         (let [left   (- cx (/ w 2))
               right  (+ cx (/ w 2))
               top    (- cx (/ h 2))
               bottom (+ cx (/ h 2))]
           [[left top] [left bottom] [right top] [right bottom]]))
#'user/rect
user=> (rect 10 10 20 40)
[[0 -10] [0 30] [20 -10] [20 30]]
user=> left
java.lang.Exception: Unable to resolve symbol: left in this context (NO_SOURCE_FILE:0)


defも名前を付けるのに使えますが、同じ名前空間のどこでも使用できます。

user=> (def x 100)
#'user/x
user=> x
100

正規表現

使えれば何かと便利です。
正規表現は文字列の頭に#を付けて表します。

マッチした部分を全て返すre-seq、
最初のマッチを返すre-find、
完全一致するかを判断するre-matchesなどがあります。

user=> (re-seq #"([-+]?[0-9]+)/([0-9]+)" "22/7 11/8 33/9")
(["22/7" "22" "7"] ["11/8" "11" "8"] ["33/9" "33" "9"])
user=> (re-find #"([-+]?[0-9]+)/([0-9]+)" "22/7")
["22/7" "22" "7"]
user=> (re-matches #"([-+]?[0-9]+)/([0-9]+)" "22/7")
["22/7" "22" "7"]

正規表現java.util.regex.Patternを借りているだけなので、
必要ならJavaメソッドを呼び出します。

Javaのクラスを使う

ClojureJavaのクラスを直接扱えます。

user=> (let [rnd (new java.util.Random)]
         (. rnd nextInt))

別記法

user=> (let [rnd (java.util.Random.)]
         (.nextInt rnd))

一々パッケージ名つきクラスを扱うのは面倒くさいので、Java同様importをします。

user=> (import '(java.util Random)
               '(java.io FileReader LineNumberReader))
user=> (.Random)

ファイル出入力

Javaで、ファイルの各行を読み込むには、このように書くようです。
(ファイルのcloseは今回、無視します)

r = new LineNumberReader(new FileReader("spam.txt"));
String line;
while((line = r.readLine()) != null){
    System.out.println(line);
}

とりあえず最初の1行を読み込んで見ましょう

user=> (import '(java.io LineNumberReader FileReader))
user=> (let [r (LineNumberReader. (FileReader. "spam.txt"))]
         (.readLine r))
"1行目の内容"

Javaではwhile文を使っているところを、Clojureではloop/recurを使って書きます。

user=> (import '(java.io LineNumberReader FileReader))
user=> (let [reader (LineNumberReader. (FileReader. "spam.txt"))]
         (loop [r reader]
           (let [line (.readLine r)]
             (if (nil? line)
               nil
               (do
                 (println line)
                 (recur r))))))

1行目の内容
2行目の内容
3行目の内容

2010/05/10追記(aaaa氏のコメントによる指摘により):
ファイルを開くときは、letの代わりにwith-openマクロを使うと、自動的に.closeを呼んでくれます。

user=> (import '(java.io LineNumberReader FileReader))
user=> (with-open [reader (LineNumberReader. (FileReader. "spam.txt"))]
         (loop [r reader]
           (let [line (.readLine r)]
             (if (nil? line)
               nil
               (do
                 (println line)
                 (recur r))))))
1行目の内容
2行目の内容
3行目の内容

さらに、loop/recurの代わりにdoseq と line-seqを使うともっとスッキリ書けます。

user=> (import '(java.io LineNumberReader FileReader))
user=> (with-open [reader (LineNumberReader. (FileReader. "spam.txt"))]
         (doseq [line (line-seq reader)]
           (println line)))
1行目の内容
2行目の内容
3行目の内容

line-seqはファイルの行のシーケンスを返します。
そしてdoseqで、行のシーケンスに対し、printlnを繰り返します。

企画倒れでした。

Rubyと同じ構成にしたのが間違いでした。

Rubyの場合、ファイルの説明には、open関数と変数、メソッドをちょっと使うだけですが、Clojureの場合、import、new、let、loop/recurと、割と高度な機能まで必要です。

あまり題材が良くありませんでした。
なにより並行処理を全くやってないですし。

Clojureをはじめる

個人的にPythonは3年間趣味に使って、それなりにやり尽くした気がするので、
新しい言語に手を出してみることにしました。(やり尽くしたとは言っても、表ラスボスを倒した程度、裏ボス・隠しボスはまだ倒してないですよ)。

選んだ言語は『Clojure
特徴:

JVM上で動作
多くの機能をJavaに依存。Javaを直接呼び出せる。
関数型
基本的なデータ型は全てimmutable(変更不可)
Lisp
マクロも好きなだけ使える。ただしリーダーマクロは追加できない。
並行処理
STM(ソフトウェアトランザクショナルメモリ、楽天的ロック)という機能を提供。Javaなどのロック機構に比べ、簡単にコードを書けてデッドロックも無い。

大学生協で買った「プログラミングClojure」を読んだ印象では、

ClojureLisp界のPython

Pythonのように、ユーザーに歩み寄ってくれるという意味です。

CommonLispの入門記事を見ると、どれもcar や cdr で始まり、延々とリストや再帰の説明が続きます。実用的な部分に到達する前に、大半の人がS式に耐えられず脱落します。まるで「Lispに体を合わせろ」と言わんばかりです。

しかし、Clojureは関数名も簡潔で、意味の無い"car", "cdr"を"first", "rest"に置き換えています。リストを過度に強調せず、ハッシュや集合型を組み込みで用意しており、また、括弧も出来るだけ減らす方向で設計されています。

またPythonと比べても、Clojureはimmutableなデータ型やSTMなど先進的な機能を搭載しています。あるいは、『2010年代のPython』となりうる言語かもしれません。