Hatena::ブログ(Diary)

達人プログラマーを目指して このページをアンテナに追加 RSSフィード

2011-06-04

Clojure言語基本の「き」(その2)

前回Clojure言語基本の「き」 - 達人プログラマーを目指してに引き続き、Clojure言語を新しく覚える際にポイントとなる事柄をまとめていきたいと思います。

逐次実行

(do 式1 式2 ... 式n)の形式を使うことで、処理を逐次実行させることができます。最終的に一番右側の式の結果が全体的な式の結果となります。この方法は副作用の存在を前提としているため、純粋な関数型のプログラムに対する手続き型プログラミングへの抜け道となる危険性がありますが、ClojureからJavaを呼び出す場合などに便利な場合があります。

user=>
(do
  (println "Hello")
  (println "World")
  (+ 1 2))
Hello
World
3

また、doと似た構造は、(後から説明するように)letやwhenなどClojureプログラムのさまざまな場面で暗黙的に表れるという点も重要です。

letによるローカル束縛

前回の記事でdefマクロを使うことで変数が定義できると説明しました。この変数関数の外部でも参照できるため、グローバル変数のようなものです。一方、特定のスコープの中だけで参照可能なローカル変数的な変数を定義する(ローカル束縛)にはlet特殊形式が使えます。これは(let [バインディング*] 式*)という形になります。

user=>
(let [radius 5, pi 3.141592, area (* pi (* radius radius))]
  (println "半径 =" radius)
  (println "面積 =" area)
  area)

半径 = 5
面積 = 78.5398
78.5398
user=> radius
java.lang.Exception: Unable to resolve symbol: radius in this context (NO_SOURCE_FILE:0)

letの後のベクターの部分で順次radius、pi、areaというローカル束縛を作成することで、円の面積を計算しています。なお、letの最後の式の部分は、doと同様に複数の式を記述でき、最後の式の値がlet式の値となります。なお、letの外側ではローカル束縛を参照することができないため、エラーとなっています。

条件分岐

もちろん、いわゆるif文やswitch文に相当する機能もあります。

まず(if test then else?)の形のif特殊形式があります。以下の例では、式(< number 100)を評価することでパラメーターnumberが100未満なら"yes"、それ以外なら"no"を返す関数を定義しています。(一般的に、booleanを返す述語関数の名前の最後に?をつける慣習に注意してください。)

user=>
(defn is-small? [number]
  (if (< number 100) "yes" "no"))

#'user/is-small?

user=> (is-small? 50)
"yes"
user=> (is-small? 200)
"no"

Javaの場合と比較してif文というよりは3項演算子の方が概念的に近いかもしれません。

public String isSmall(int number) {
    return (number < 100) ? "yes" : "no";
}

javaのif-else文のようにthenやelseの部分で複数の処理を実行させたい場合は明示的にdoを入れ子にして利用する必要があります。

次に、ifと似たものとしてwhenマクロがあります。これは(when test 式*)の形式となります。

user=>
(defn is-small? [number]
  (when (< number 100) (println "number = " number) "yes"))

#'user/is-small?
user=> (is-small? 30)
number =  30
"yes"
user=> (is-small? 100)
nil

ifと違って、こちらはelseが記述できない代わりに、最後の部分が暗黙doによって複数の式が実行できることになります。この他、判定条件が逆転したif-notやwhen-notも時として便利です。

最後に、switchに相当するものとしてcondマクロが使えます。

user=> 
(defn number-size [number]
(cond 
  (< number 50) "small"
  (and (<= 50 number) (< number 100)) "midium"
  :else "large"))

#'user/number-size
user=> (number-size 30)
"small"
user=> (number-size 50)
"midium"
user=> (number-size 100)
"large"

コレクションに対する関数

逐次、分岐の説明が終われば、普通の手続き型の言語ではループを使った繰り返し処理の説明になるのですが、Clojureの場合は強力なコレクション関数が使えるため、低レベルのループ(実際には再帰アルゴリズムを記述するケースは非常にまれになります。中でも、以下の関数が代表的です。

map関数

map関数を使うとコレクションの各要素をパラメータとして適用した結果を各要素とする新しいシーケンスが生成されます。たとえば、与えられた引数を2倍にする以下の関数を考えます。

user=>
(defn double-num [n] (* 2 n))
#'user/double-num
user=> (double-num 3)
6

そして、ベクター[1 2 3 4 5]の各要素を2倍にするには以下のように記述することができます。

user=>(map double-num [1 2 3 4 5])
(2 4 6 8 10)

ここで、map関数関数型言語において高階関数と呼ばれるものの例になっていて、他の関数自体をパラメーターとして取得して処理している点に注意してください。それから、先の例ではベクターリテラルで数列を渡しましたがこのように連続した数値の列はrange関数を使って生成するのが便利です。以上の例は以下のようにも書けます。

user=>(map double-num (range 1 6))
(2 4 6 8 10)

以上の例ではわざわざdouble-num関数を別途defnで定義して実行していたのですが、JavaScriptと同様にfn特殊形式を使って無名関数を利用することもできます。今回のように単に値を倍にするだけなら以下のように書けます。

user=>(map (fn [n] (* 2 n)) (range 1 6))
(2 4 6 8 10)

(fn [n] (* 2 n))の部分が無名関数となっています。さらに、これはもっと短縮して以下のようにも記述できます。

user=> (map #(* 2 %) (range 1 6))
(2 4 6 8 10)

実際には#(* 2 %)の部分が(fn [n] (* 2 n))に展開されることになります。

reduce関数

mapと並んで重要な関数としてreduce関数があります。これはmapと比べて最初動作が理解しにくいのですが、2つのパラメーターを取る関数fに対して(reduce f coll)とした場合、collの最初の2つの要素に対してfを適用し、次にその結果とcollの3番目の要素にfを適用という順番で順次計算した結果を返します。結果はスカラー値になります。これを理解するには例を見るのが一番で、1から10までの数値を足し算するには以下のように記述できます。

user=> (range 1 11)
(1 2 3 4 5 6 7 8 9 10)
user=>(reduce + (range 1 11))
55
apply関数

前回のエントリでも説明したように実際には+関数は可変長の任意の数値を取ることができるので、実際にはreduceを使って2つずつ順番に足し合わせなくても、任意の個数の数値の合計を算出することができます。

user=> (+ 1 2 3 4 5 6 7 8 9 10)
55

でも、以下のようにするとエラーになってしまいます。

user=> (+ (range 1 11))
java.lang.ClassCastException (NO_SOURCE_FILE:0)

+関数は複数の数値をパラメーターとして取ることはできてもコレクションを取ることはできないからです。この場合、apply関数を使うと、コレクションの各要素を可変長パラメータとして取り出して渡すことができます。

user=> (apply + (range 1 11))
55
filerとremove

filer関数を使うと述語関数として指定した条件を満たす要素のみから構成されるシーケンス*1を作成できます。逆にremove関数を使うと、条件を満たさないシーケンスが生成されます。

user=>(def col (range 1 11))
#'user/col
user=> col
(1 2 3 4 5 6 7 8 9 10)
user=> (filter #(> % 5) col)
(6 7 8 9 10)
user=> (remove #(> % 5) col)
(1 2 3 4 5)

recurとloopによる再帰

前節で説明したコレクションに対する高階関数をうまく使うことで、難しい再帰アルゴリズムを書くケースは減るはずですが、再帰こそがLispの一種であるClojureらしいところでもあります。再帰の詳しい説明は別の機会にすることにしてここではごく基本的な例のみ紹介します。

単純にパラメーターで与えられた数値まで自然数を足し合わせる処理を再帰によって実装するには以下のような関数が定義できます。

user=>
(defn sum-down-from [sum x]
  (if (pos? x)
    (recur (+ sum x) (dec x))
    sum))

#'user/sum-down-from
user=> (sum-down-from 0 10)
55

recur特殊形式を使うと自分の関数再帰的に呼び出すことができます。以上の例では、xの値が正の間(pos?関数がtrueの間)再帰的にxを一つ減らした値で呼び出され、最後にsumが返されるため、結果として合計が計算されます。なお、素直に考えると以下でも同じような気がしますし、実際これでも同じ結果になります。

user=>
(defn sum-down-from2 [sum x]
  (if (pos? x)
    (sum-down-from2 (+ sum x) (dec x))
    sum))

#'user/sum-down-from2
user=> (sum-down-from2 0 10)
55

以上の例ではrecur特殊形式を使う代わりに普通に自分自身を再帰的に呼び出しています。しかし、内部的な動作は両者で異なり、recurを使った場合は、末尾再帰最適化されるため、大量のスタック領域を使うことを避けられるという違いがあります。

user=> (sum-down-from 0 10000)
50005000
user=> (sum-down-from2 0 10000)
java.lang.StackOverflowError (NO_SOURCE_FILE:0)

なお、再帰する際に関数の先頭に戻りたくない場合、ループする範囲をloop特殊形式で指定することができます。loopはrecurの対象となる点を除くとletとまったく同じ形をしています。以下は階乗を計算しています。

user=>
(defn factorial [n0]
  (loop [fact 1, n n0]
    (if (pos? n)
      (recur (* fact n) (dec n))
      fact )))

#'user/fact
user=> (factorial 3)
6
user=> (factorial 5)
120

loopを使うことで呼び出し側には公開したくないローカルな変数が使えます。

クオート

一般的にはClojureの式は直ちに評価されます。特にシンボルはvarに対する束縛があればvarの値になるし、そうでなければエラーになります。また、(a b c)の形のリストは今での例で見てきたように(特殊形やマクロを除いて)関数呼び出しとして解釈されます。

user=> (def a "test")
#'user/a
user=> a
"test"
user=> b
java.lang.Exception: Unable to resolve symbol: b in this context (NO_SOURCE_FILE:0)
user=> (1 2 3)
java.lang.ClassCastException: java.lang.Integer cannot be cast to clojure.lang.IFn (NO_SOURCE_FILE:0)

ここで、最初のaは束縛されているvarの値"test"として評価されていますが、bは束縛が存在しないためエラーとなっています。最後のエラーは奇妙ですが、1が関数名として解釈されてしまった結果、これが関数型にキャストできないためエラーになっています。このような式の評価を抑制させるためにはquote特殊形式を使うことができます。

user=> (quote a)
a
user=> (quote b)
b
user=> (quote (1 2 3))
(1 2 3)

なお、リーダーマクロという仕組みにより一重引用符「'」を先頭につけるとquoteに展開されるため、同じ意味になります。

user=> 'a
a
user=> 'b
b
user=> '(1 2 3)
(1 2 3)

なお、似て非なる概念としてバックティック記号「`」を使った構文クオートという仕組みがありますがここでは説明しません。

varと名前空間

今までdefマクロを使うことで「変数」が定義できると説明してきました。

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

これをもう少し詳しく説明すると、defにより、値100を保持するvarがデフォルト名前空間であるuserに生成され、それがシンボルaによって束縛されたということになります。*2この場合シンボルaはvarの単純名のように、現在の名前空間にしたがって一旦user/aとして解決されてから評価されます。ですから、以下の結果は同じになります。

user=> a
100
user=> user/a
100

名前空間Javaのパッケージのような感じで、同じ単純名のvarを別々の名前空間に生成できるので、大規模なプログラムを作成する際に名前の競合を避けることができます。現在の名前空間を変更するにはin-ns関数が使用できます。*3

user=> (in-ns test)
nil
test=> a
java.lang.Exception: Unable to resolve symbol: a in this context (NO_SOURCE_FILE:0)

現在の名前空間が変更されると、もう単純名aでもともとのvarを参照することはできなくなります。シンボルaがtest/aとして解決されるようになるためです。そして、以下のように同じ単純名aで別のvarをtest名前空間に作成することができます。この場合、もともとのuserに属していたvarなそのまま残っているため、完全名で参照することもできます。

test=> (def a "TEST")
#'test/a
test=> a
"TEST"
test=> user/a
100
test=> 

完全名が名前空間名/単純名の形式になっていることからもわかるように、名前空間ファイルシステムのフォルダーのようなものと考え、varは値を保持するファイルのようなものでその名前がdefでシンボルと束縛されていると考えると理解しやすいのではないでしょうか。もちろん、名前空間は階層化できます。

test=> (in-ns test/child)
nil
test/child=> (def a "Hello World")
#'test/child/a
test/child=> 

useとrequire

Javaのimportと同じように他の名前空間にあるシンボルを自分の名前空間で単純名で参照させることができます。頻繁に使用する他の名前空間に属しているシンボルを単純名で参照できると便利です。他の名前空間の読み込みに関連する関数としてrequireとuseがあり、最初は両者の使い分けがなかなか頭に入りにくいところがありますが、

という意味になります。以下の実行結果を見てください。(requireやuseで引数に対してクオートを使っている点に注意)

user=> (clojure.contrib.math/round 1.7)
java.lang.ClassNotFoundException: clojure.contrib.math (NO_SOURCE_FILE:0)
user=> (require 'clojure.contrib.math)
nil
user=> (clojure.contrib.math/round 1.7)
2
user=> (round 1.7)
java.lang.Exception: Unable to resolve symbol: round in this context (NO_SOURCE_FILE:14)
user=> (use 'clojure.contrib.math)
nil
user=> (round 1.7)
2

requireをする前は読み込み自体がされていないので、完全名を指定しても実行できません。require実行後は完全名を指定したときのみ実行できます。最後にuseを利用するとround関数自体がuser名前空間で単純名として参照できるようになります。以上のようにuseするとclojure.contrib.mathにある全シンボルが現在の名前空間から参照できるようになってしまうため、以下のように特定のシンボルのみuseすることもできます。

user=> (use '[clojure.contrib.math :only (round)])
nil

なお、REPLで対話的に名前空間を読み込むのではなく大きなプログラムClojureソースコードとして作成する場合はプログラムの先頭でnsマクロを使って宣言するのが一般的なようです。

(ns test
  (:use [clojure.string :only [capitalize]]))

以下の文献を参考にしました。

The Joy of Clojure: Thinking the Clojure Way

The Joy of Clojure: Thinking the Clojure Way

プログラミングClojure

プログラミングClojure

*1:シーケンスとはベクター、リストなどのコレクションの違いを抽象化した概念

*2:全てのスレッドから参照できるのでルート束縛と呼ばれる。

*3:代わりにnsマクロを使うこともできる。この場合はデフォルトインポートされるパッケージが異なる。nsマクロREPLの対話的処理でなく、Clojureのソース中で名前空間を宣言する場合に便利。

2011-05-15

Clojure言語基本の「き」

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

スカラー値のリテラル

ClojureREPL(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ではカンマ「,」は空白やスペースと同じように無視される文字なので、お好みで以下のように記述することも可能です。こちらの書き方のほうがGroovyJava 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の場合は例外となります。

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

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

*8:ALGOL系言語の

2011-05-05

ふと思い立ってClojureの勉強を始めることにしました。

最近は並行プログラミングの必要性から、関数型言語の人気が以前にもまして高まってきているという話はいろいろなところで耳にするようになったのですが、普段はJavaしか書かない私のような普通の業務プログラマーにとっては、正直なところHaskellやScalaといった言語はなかなかに敷居が高いところがあると思います。これらは型安全性を重視している静的な型付けの言語であり、プログラムを正しく実行するには型推論などの仕組みを理解する必要があります。また、純粋な関数型としての特性を理解するにはモナドなる難解な概念を理解しないといけないようですし、特に、Scalaの場合はオブジェクト指向言語の要素がミックスされているため、クラス、トレイト、抽象型などの概念や、関数型と命令型の正しい使い分けなどを理解しなくてはなりません。*1とにかく、これらの言語では関数型を学ぶ以前にいろいろ新しく学ぶことがたくさんあるため、自力で最初に学ぶ関数型言語としてはなかなか難しいところがあるように思います。実際、フレームワーク、ツール、ドメインの業務知識などいろいろなことを勉強しないといけない業務プログラマーの立場としては、プログラミング言語の勉強だけに長い時間を割くということが難しいという実情もあります。

そんな中で、

のようなことを何となく考えていたのですが、残念ながらGroovyだとどうしても関数型のメリットをはっきりと活かすようなプログラミングが難しいのも確かです。何か関数型の考え方を勉強するのによい言語はないかなと考えていた折、ちょうどよいタイミングでスターフレンドのid:fumokmmさんの

というつぶやきが目にとまりました。GroovyだけでなくClojureまで知っているとはすごいなと思ったのですが、ご謙遜かもしれませんが、

というお言葉を信じて、私も翻訳協力者に混ぜていただく運びとなりました。その翻訳サイトは以下にあります。

Clojure Japanese Documentation

ドキュメントを翻訳しながら新しい言語も学ぶというのは、最終的な成果物もみんなの役に立って、自分の勉強にもなるのでなかなかに良いアイデアですね。ただ、さすがにまったく基礎知識なしで翻訳を進めるのは無謀なところがあるので、本日書店で早速以下を購入して勉強することにしました。*2

プログラミングClojure

プログラミングClojure

ちょっとかじったところでは、

  • 文法はほとんどLispLispの方言と言える)
  • クラスなどは定義できないから関数型のパラダイムに集中できる
  • Javaのメソッドを簡単に呼び出せる(静的な型も使えるということ?)
  • 純粋関数型以外に状態共有のメカニズムがある

といったところが特徴のようです。実際、hello worldは以下のように書けます。

(println "Hello World") ; Hello Worldと表示される。結果はnil。

クラスやメソッドの定義といったものを考える必要がなく、単純な関数の評価なのでわかりやすいですね。一方で、Javaのメソッド呼び出しは、以下のように書けます。

(println (.toUpperCase "Hello World")) ; HELLO WORLDと表示される。結果はnil。

括弧の先頭で与えるシンボルが関数名だと関数呼び出しとなるのですが、この関数名がピリオドで始まっていると次の要素に対するメソッド呼び出しとなるようです。Javaの呼び出しなどで関数型としての厳密性はなくなっているのですが、既存のJavaのライブラリーと相互運用できるというところは実用面では非常にメリットが大きいと思います。Lispの独特の構文に慣れる必要はありますが、直感的にはなんとくなく私が求めていた言語に出会えた気がします。ちなみに、日本ではまだまだ知名度が低いようですが、既に洋書は多数出版されているみたいですし、あのDDDのEvans氏もお好きとのこと。これからあせらず、じっくり勉強していこうかと思います。なお、以上に紹介したClojureの本はJava EE勉強会で読書会が行われているみたいですので、東京近郊の方は参加してみてはいかがでしょうか。

FrontPage - Java EE勉強会

あと、以下も読書記録として非常に参考になると思います。

clojureプログラミング入門-1 clojureをはじめてみる - すにぺっと

*1:Scalaは名前のとおりスケーラブルな言語であり、入門レベルではとっつきやすいプログラムを書けるのですが、言語を究めてきちんと使いこなそうと思えば多くの概念を学ぶ必要があるようです。

*2プログラマーの定年を過ぎてまったく新しい言語を学ぶというのは何とも無謀な試みのようなのですが、気持ちだけは若いつもりです。