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

プログラミングClojure1章を読んでみた

Clojure プログラミング

前から関数型言語に興味があり、LispやF#を勉強しました。
Lispはとても面白い言語ではあるものの、ライブラリの貧弱さから、
これで実際にアプリケーション作成するのは厳しいと感じました。
F#は.NET Frameworkが利用できるため、実用性の観点から言っても申し分無かったのですが、
勉強すればする程「これC#で良いのでは…?」という思いが募ってきました…
C#が既に関数型言語っぽい機能を豊富に持っているというのもやる気を減退させた一因)。


ClojureLispのような同図像性(自分自身を定義できる。LispLispを書いて好きなように拡張出来る)を持ちつつ、
JVM上で動き、Javaの豊富なライブラリを利用可能であり、
Lispの柔軟性とJavaの実用性を併せ持った面白い言語ということでチャレンジしてみることにしました。
教科書はプログラミングClojure。前書きから著者の強烈なClojure愛を感じたのが決め手。


ではClojureの勉強を始めましょう!
(※以下はWindows Vistaで実行しています)


Clojure本体をここからダウンロード。
とりあえず勉強始めたてなのでStable Release: 1.2.1をDL
http://clojure.org/downloads


次に、サンプルを以下からダウンロード。
http://pragprog.com/titles/shcloj/source_code
サンプルにコアであるClojure.jarが同梱されているため、
サンプルを試したいだけなら上記本体落とさなくてもOK。
ただ、同梱されているClojure.jarはverが古いので一応落としておきましょう。
サンプルのcode/bin/repl.batをコピーしてcode直下に置きます。
repl.batを叩くとClojureが起動します。


まずはお約束、Hello World.
(println "hello world")
ちなみに
(str "hello world")
でもHello World出来ます。
さて、printlnとstr、どう違うのでしょうか?
不明点があれば(doc オブジェクト名)で調べてみましょう。

user=> (doc println)

                                                • -

clojure.core/println
([& more])
Same as print followed by (newline)

user=> (doc str)

                                                • -

clojure.core/str
([] [x] [x & ys])
With no args, returns the empty string. With one arg x, returns
x.toString(). (str nil) returns the empty string. With more than
one arg, returns the concatenation of the str values of the args.

printlnもstrも引数を表示する関数のようですが、
後者は複数の引数を与えると、それらを連結した文字列を返すようですね。
試して見ましょう。

user=> (println "hello" "world")
hello world
user=> (str "hello" "world")
"helloworld"

というわけで、(doc オブジェクト名)で不明点を調べることが出来るようになりました。
更に、オブジェクト名が曖昧なときなどは(find-doc "検索条件(調べたい文字列 or 正規表現)")で
検索条件にヒットしたドキュメントが表示されます。


電卓代わりに使ってみましょう
(+ 1 2)
はい、3と正しく返されましたね。
Lisp(と言うかポーランド記法)に慣れないうちは「…なんだこの記法?」と驚きました。
Clojureは全てが関数です。
つまり、足し算の演算子+すら関数名です。
(doc +)を試して見ると

user=> (doc +)

                                                • -

clojure.core/+
([] [x] [x y] [x y & more])
Returns the sum of nums. (+) returns 0.

つまり、+は引数全てを足し合わせた数値を返す関数です。
なので、

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

こんな事も可能です。
勿論足し算だけではなく、他の演算も可能です。
(* 2 3)
なら6が返ってきますし、
(/ 6 3)
なら2が返ってきます。
ちょっと驚いたのが
(/ 1 2)
が1/2と返ってくることです。
0.5と返して欲しければ
(/ 1 2.0)
とします。
これもdoc関数で調べてみましょう。

user=> (doc /)

                                                • -

clojure.core//
([x] [x y] [x y & more])
If no denominators are supplied, returns 1/numerator,
else returns numerator divided by all of the denominators.

なるほど…こう来るとは…想定外でした…。


では関数作成。
関数を定義するにはdefnを利用します。

user=> (defn hello[s] (str "hello " s))
#'user/hello
user=> (hello "AntiBayesian")
"hello AntiBayesian"

ClojureLispと異なり、引数は[]で指定します。これにより、どれが引数なのか一目瞭然になりました。



落ち穂拾いとして、ちょっと変わった変数を。*1, *2, *3は直前の結果が格納されています。
つまり

user=> (str "hello")
"hello"
user=> (str "world")
"world"
user=> (str *2 " small " *1)
"hello small world"

となります。
また、*eは直前のエラー情報を格納しており、

user=> (/ 1 0)
java.lang.ArithmeticException: Divide by zero (NO_SOURCE_FILE:0)
user=> (str *e)
"java.lang.ArithmeticException: Divide by zero (NO_SOURCE_FILE:0)"

となります。


ファイルに書き込んだ処理を実行するには
(load-file ファイル名)とします。
ファイルに(str "success!")と記述し、test.cljと名前を付けてcode直下に置きます。
そして
(load-file test.clj)
で"success!"が表示されます。


さて、(純粋)関数型言語の特徴として参照透過性が挙げられます。
簡単に言うと、変数の中身を変更できないことを指します(すげー大雑把な説明)。
「なんでそんな不便なことを!?」と思われるかも知れませんが、
変数の中身が不変であるからこそ、ロジックが追いやすくなり、
コーディングやメンテナンスが楽になる…らしいです
(そうかなぁ?だったらさっきの*1とか*eとか何なんだろう)。
Clojureも基本的には変数の中身を変更することは出来ません。
しかしそうは言っても、どうしても変更したい時だってあるじゃん!!!
ということで、「面倒臭い手続きを踏むと一応変えられなくはないよ〜」
というのがClojureのスタンスっぽいです。
C#PHPなら
i = 10;
なんて感じでごく簡単に変更出来ましたが、Clojureではどのように参照透過性を破るのでしょうか。

(def visitors(ref #{"mike"}))
(dosync (alter visitors conj "Ken"))

という長ったらしいことをしないと変更出来ないそうです。本当に?
@変数名
とすると変数の中身が表示されます。
今は当然@visitors とすると "mike" "Ken" ですね。
でもこれ
(def visitors(ref #{"mike" "Ken" "marry"}))
ってすると普通に
#{"marry" "Ken" "mike"}
となるし、単に定義し直せば良いのでは…。

user=> (def myName(ref "KEN"))
#'user/myName
user=> @myName
"KEN"
user=> (def myName(ref "mike"))
#'user/myName
user=> @myName
"mike"

とまぁこのように、定義し直せば無理矢理変数っぽく使えるのでは?
あれ、参照透過性って「最初に(←ココ重要)定義した値から変更出来ない」ことを保証してくれるんじゃなかったっけ…。
これだと任意の場所で再定義出来るから、どこかで再定義したら関数の振る舞い変わる…。
うっかり新しい定義だと思って同じ名前のものを定義してしまったら…

user=> (defn help[s](if (s "Ken") (str "hello")))
#'user/help
user=> (def visitors(ref #{"mike" "Ken" "marry"}))
#'user/visitors
user=> (help visitors)
"hello"
user=> (def visitors(ref #{"mike" "marry"})) ;ここでうっかり再定義してしまったとしよう
#'user/visitors
user=> (help visitors)
nil;振る舞いが変わってる!


user=> (defn help[s](if (s "Ken") (def visitor(ref #{"mike" "marry"}))));破壊的再定義
#'user/help
user=> (def visitor(ref #{"mike" "Ken" "marry"}))
#'user/visitor
user=> (help visitor)
#'user/visitor
user=> @visitor
#{"marry" "mike"};破壊されてる…

…わからん。
ちなみに(s "Ken")の部分はsに"Ken"が含まれている場合は"Ken"を返し、含まれていない場合はnilを返す。
ググってもよくわからないが、後の章で何か出てくるのかも知れないし、とりあえず置いておく。
※tyatutaさんから『それは「シャドウイング」です。
「定義し直し」ているというよりは「同じ名前の変数を作って、もとの変数を隠している」のです』
というご助言を頂きました。有難うございました!
※※再度質問したところ、どうも私が疑問にしている箇所は
シャドウイングではないっぽいことが段々わかってきて、
焦らず2章読み進めていった方が良いとのことでした。重ね重ね有難うございます。


ライブラリを用いるには(use 'ライブラリ名)とする。
正常にライブラリを読み込んだ場合はnilが返ってくる。
code/examplesフォルダ内にフィボナッチ数を計算するサンプルが入っている。

user=> (use 'examples.introduction)
nil
user=> (take 10 fibs)
(0 1 1 2 3 5 8 13 21 34)

user=> (use 'clojure.contrib.repl-utils)
nil
user=> (show java.util.HashMap)
=== public java.util.HashMap ===
[ 0] ()
[ 1] (Map)
.
.
.
(続いてる)

というところで一章終わり。
長かった…。特に参照可能性の所。
まさか、そんな…と思ってテストコード書こうにも、
今先程見たばかりの言語だから、テストコード書こうにも覚束ない。
参照透過性について自分が全くの勘違いしていること気付けて良かったです。


ここまでの感想
また1章(23ページ)までしか読んでないけど、面白い!
JavaAPIを簡単に叩けるようだし、
better Lispとして書ける。
一日半章ペースでこれからも読み進めていきたいですね。