Clojure ことはじめ① - 導入 / データ構造 -
Clojureについて
Clojure は JVM をプラットフォームとして動作するLispだ.Clojure というイカした名前にはいくつかの由来がある.ひとつめは,同音異義語である 'closure'.日本語でいうと閉包.この言葉,数学では位相空間論で登場するが,プログラミングの文脈では ’自由変数を関数内に閉じ込める役割を持った無名関数’のことで,高階関数の引数としておなじみ.まあ,今のところはそんなに気にしなくていい.ふたつめは,Common Lisp.Scheme と並んで広く浸透している Lisp のひとつ.Lisp には名前空間の数に対応して,Lisp-1, Lisp-2 の二種類があり,Scheme は Lisp-2 ,Common Lisp は Lisp-1,Clojure は Lisp-1 だ.名前空間とは何か,そもそもLisp って何か,という話はひとまず置いておこう.Clojure という名前について,Common Lisp からは 'C' と 'L' を拝借している.みっつめは,Java.お察しの通り,これは Clojure の 'J' にあたる.先述の通り,Clojure は JVM (Java Virtual Machine) を言語プラットフォームとして採用していて,この点で他の Lisp とは異質だ.というのは,ほとんどの Lisp はそれぞれ独自のプラットフォームを持っている.Clojure を '実用的な' Lisp と評する向きはおおむねこの,JVM 上で動作する,という点に着目してのことだろうと思う.Clojure と Java の蜜月はかなりのもので,詳しくは後述するが,Java を自在に操作することは,必須とは言わないまでも,目下の問題に対する Clojure によるアプローチをより豊かなものにするだろう.ひとつめの 'closure' はダジャレ的な意味合いが強いものの,(Common) Lisp と Java は Clojure という言語の設計や思想に直接的な影響を与えている.ではその設計や思想とはなんぞや,という話になるが,僕自身そこまで深く理解している自信がないので,おいおいまとめようと思う.また,REPL駆動開発や,Leiningen によるビルド方法など,具体的な開発手法や開発環境に関しては公式のドキュメントを鋭意翻訳中なので,そちらに譲る.
シンタックス
Clojure のシンタックスはとてもシンプルだ.そして,Clojure のシンプルさは Lisp に由来する.統一的な構造,両手で足りるほどに厳選された特殊オペレータ,そしてちょっと目立ちがちな,親愛なるカッコたち.他の言語に触った経験があるひとならみんな,このカッコの量に最初はちょっと面食らうかもしれない.でも大丈夫,すぐ慣れる.カッコの管理に必要なコストはきっと思っているよりずっと少ないし,エディタなりIDEなりを導入すれば,対応関係をよしなに処理してくれる.何より,カッコは便利だ.カッコがなぜ,どんなふうに便利なのかは,これから学んでいく中で分かってくると思う.だから今はとりあえず,カッコは友達だってことを忘れないで欲しい.
フォーム
フォームというのは,ざっくりと式のことだと思えばいい.実際,色々なドキュメントで,フォームと式は同じものとして扱われている. もっとも,フォーム(ないし式)をそれ単体で記述する機会は稀で,たいていの場合,特定のオペレーションの中で用いることになる.以下,稀な機会としていくつかのフォームを単体で書き並べてみた.
2 ;;数値 "serial experiments lain" ;;文字列 ["a" "vector" "of" "serial" "experiments" "lain"] ;;ヴェクタ
なんとも落ち着きが悪い感じがする.もしそう感じないのであれば,これから登場する他のサンプルコードをいくつか見てから,改めて上の例を見てみるといいかもしれない. オペレーションについては統一的な書式があるので,まずそれを紹介する.
(operator operand1 operand2 ... operandn)
まず '(' ,その直後にオペレータ,それ以降はオペランドで,最後に ')' で閉じる.ひとつのオペレーションにつき,オペレータがひとつなのに対して,オペランドはいくつあってもいい.とにかくこの,カッコで閉じられたひとかたまりが Clojure のオペレーションだ.Lisp の例にもれず,Clojure もオペランドの区切り記号として原則的に空白を使う.
(+ 1 2) ;=> 3 (* 2 3 4 5 6) ;=> 720 (str "close the world, " "open the next") ;=> "close the world, open the next"
原則的にと書いたのは,視認性のため ' , ' を使う場合もあるからだ.これは後ほど紹介する.ここでは簡単に上の例を説明しよう.まあ,最初の二つに関しては,説明するまでもなく一目瞭然だろうけど.最初の例では +
というオペレータが 1
と 2
というオペランドに適用されて,3
と評価されている.'適用(application)' とか '評価(evaluation)' とかいう言い回しは関数型プログラミングに特徴的なものだけど,さしあたりそこまで気にする必要はない.
制御フロー
ここでは,if
,do
,when
という3つの制御フローを紹介する.
if
まず,if
フォームの基本的な形はこんな感じ.
(if test-form then-branch else-branch)
Clojure は if
オペレータを見つけると,直後のtest-form
をまず評価する.test-form
はブール値として true
か false
に評価される.test-form
が true
と評価された場合 Clojure は then-branch
を評価し,false
の場合は else-branch
を評価する.
この辺り,Clojure の真偽判定はちょっと特殊で,nil
と false
だけが偽,その他の場合はすべて真となる.
(if nil "Stand Alone" "Complex") ;=> "Complex" (if false "Stand Alone" "Complex") ;=> "Complex" (if "Ghost in the Shell" "Stand Alone" "Complex") ;=> "Stand Alone" (if 0 "Stand Alone" "Complex") ;=> "Stand Alone" (if [] "Stand Alone" "Complex") ;=> "Stand Alone"
else-branch
はオプショナルで,なくてもいい.ちなみに,test-form
が偽で,かつ,else-branch
がない場合はnil
が返る.
(if nil "Stand Alone") ;=> nil
ご覧のとおり,Clojure の if
はオペランドの配置順序が分岐先に関連付けられているので,then-branch
も else-branch
も,それぞれひとつのフォームしか持つことができない.これは評価結果の一意性という点ではすごく嬉しいことなのだけど,実際,ちょっと不便だ.そんな不便を解消するために,Clojure は do
というオペレータを用意している.
do
if
の中で do
を使えば,複数のフォームをカッコで包んでそれぞれ実行することができる.例えばこんな感じ.
(if true (do (println "See you space cowboy...") "Rest in Peace") (do (println "See you cowgirl") "Someday, Somewhere!")) ;=> See you space cowboy... ;=> "Rest in Peace"
このように,do
によって if
の各分岐先で複数の処理を実行できる.上の例では,println
によって 'See you space cowboy...' がREPL上に表示され,if
というフォーム全体の評価結果として "Rest in Peace"
という文字列が返されている.お察しのとおり,if
と do
はしばしば使われる組み合わせだ.そこで,このふたつの機能をひとつにまとめたものが存在する.それが when
だ.
when
when
に else-branch
はない.英語における when という関係詞の用法から考えても,これは自然な振る舞いだ.
when
は test-form
が true
に評価されると,以降のフォームを順次実行する.test-form
が false
の場合は nil
を返す.
(when true (println "See you space cowboy...") "Rest in Peace") ;=> See you space cowboy... ;=> "Rest in Peace"
もちろん then-branch
がなく,else-branch
だけのバージョンも存在し,その名もズバリ when-not
.
(when-not false (println "See you cowgirl") "Someday, Somewhere!") ;=> See you cowgirl ;=> "Someday, Somewhere!"
等価性
Clojure では,評価結果がブール値 (true
/false
) になる関数の末尾には '?' をつける習慣がある.例えば,オペランドが nil
かどうかを判定するのは nil?
という関数だ.
(nil? 123) ;=> false (nil? false) ;=> false (nil? nil) ;=> true
オペランドが nil 以外のすべての場合に true
を返す some?
という関数もある.これも,英語の some のニュアンスをよく反映している.' 何かしらの値が存在する?どう?' というわけだ.(余談だけど,Clojure の組み込み関数の名前は粋で洗練されたものが多い.これはひとえに言語の作者である Rich Hickey の卓抜な言語センスの賜物だ)
(some? nil) ;=> false (some? false) ;=> true (some? []) ;=> true (some? "anything but nil") ;=> true
Clojure における等価性判定オペレータは =
に統一されている.型ごとに =
相当のオペレータを割り当てている言語もあるが,Clojure ではとりあえず何でもかんでも =
で判定できる.ありがたい.
(= 5 5 5) ;=> true (= nil nil) ;=> true (= 8 9) ;=> false
and / or
Clojure のブールオペレータとして and
と or
がある.or
はオペレーション内で最初に真と評価された値,もしくは最後の値を返す.and
は最初に偽と評価された値を返すが,もしオペレーション内に偽となる値がなかった場合,最後に真と評価された値を返す.と言われてもなんのこっちゃ,という感じだと思うので,具体例を見てみよう.まずは or
から.
(or nil false :the-first-truthy-value "the second truthy value") ;=> :the-first-truthy-value (or (= 1 2) (= "fooly" "cooly")) ;=> false
最初の例で,nil
,false
は両方とも偽となる値だ.そこで or
はオペレーション内で最初に真と評価された(つまり nil
でも false
でもない) 値である:the-first-truthy-value
を返した.次の例では,(= 1 2)
が false
,(= "fooly" "cooly")
も false
なので,全体は (or false false)
となる.false
は偽なので,このオペレーション内のオペランドはすべて偽と評価されたことになる.ということで,最後尾の値である false
が or
オペレーション全体の評価結果として返されている.
続いて,and
を見ていこう.
(and "Yang Wen-li" "Reinhard von Lohengramm" (= "Hilda" "Annerose") nil) ;=> false (and :Final-Fantasy :Chrono-Trigger :Dragon-Quest :Dark-Souls) ;=> :Dark-Souls
最初の例で,最初に偽と評価されるのは (= "Hilda" "Annerose")
だ.全体として,この評価結果である false
が返される.皇帝の妻と皇帝の姉が同一人物であるという命題は偽,当然の結果だ.and
は最初に偽と評価された値を返してそれ以降の評価をやめるので,この例で最後尾の nil
は無視されている.
次の例では,and
オペレーション内に偽となる値は存在しない.そこで,最後に評価された :Dark-Souls
が返されている.挙げた他の作品と比べてダークソウルがことさら名作だと主張したいわけではもちろんない.
名付けと束縛
Clojure では,値に名前を束縛(bind)する時に,def
を使う.束縛,というのは,他の言語でいうところの代入に近い概念で,関数型パラダイムを採用している言語では一般的な用語だ.変数に値を代入するのではなく,値に名前を束縛する.あまり難しく考える必要はないけれど,'代入' という言葉に引っ張られると後々の誤解,混乱の元となるかもしれないので,'束縛' という表現に慣れてほしい.とりあえず,def
による束縛の一般形を見てみよう.
(def symbol value)
とてもシンプル.symbol
部分のシンボルが,value
の値として束縛される.ここでも,(operator operands)
というルールが保たれていることにも注目したい.def
というオペレータを 2つのオペランドに適用しているだけで,値に名前を束縛できる.足し算や文字列の結合の時とのシンタックス的な違いはなく,書き方も共通.これが Lisp のパワーだ.では実際に def
を使ってみよう.
(def major "Motoko Kusanagi") ;=>#'user/major (def section-9 ["Aramaki" "Motoko" "Batou" "Togusa" "Ishikawa" "Borma" "Saito" "Pazu"]) ;=>#'user/section-9 major ;=> "Motoko Kusanagi" section-9 => ["Aramaki" "Motoko" "Batou" "Togusa" "Ishikawa" "Borma" "Saito" "Pazu"]
ここで,さっき少し触れた '束縛' という表現について簡単に説明しておく.例えば Ruby で,複数の変数に値を代入して次のようなコードを書いたとする.
you_say = :stop hello_goodbye = "I say YES, You say NO, You say " if you_say == :stop hello_goodbye = hello_goodbye + "Stop, and I say Go Go Go!!!!! " else hello_goodbye = hello_goodbye + "LOVE lain LOVE lain LOVE lain LOVE lain LOVE lain" end
むむむ.こんなふうに,変数代入というやり方で名前に紐づいた値を変更すると,プログラムの挙動を理解するのが難しくなる.なぜって,いちいち名前と値の関連を覚えておかなくてはいけないし,そもそもなぜその値が変わるべきなのかも知っていなくてはいけない.これはちょっと面倒だ.そしてあらゆる面倒はバグの元でもある.しかし,変数に代入,というアプローチではどうしてもこうした状況に陥りがちだ.そこで,関数型プログラミングの手法が鮮やかに活きてくる.Clojure に親しんでいくうちに,名前と値を絡めてしまう書き方から抜け出すいい方法が身に付くだろう.例えば,上のコードは Clojure でこんな風に書ける.
(defn hello-goodbye [you-say] (str "I say YES, You say NO, You say " (if (= you-say :stop) "Stop, and I say Go Go Go!!!!!" "LOVE lain LOVE lain LOVE lain LOVE lain LOVE lain"))) (hello-goodbye :stop) ;=> "I say YES, You say NO, You say Stop, and I say Go Go Go!!!!!" (hello-goodbye :why) ;=> "LOVE lain LOVE lain LOVE lain LOVE lain LOVE lain"
ここでは,hello-goodbye
という1引数の関数を定義している.仮引数であるyou-say
の値によって分岐が決定され,分岐の決定がそのまま,返される文字列に対応する. 試しに :stop
を you-say
に束縛してみると,test-formの (= you-say :stop)
が真になり,if
の性質から if
オペレーションの全体は,then-branch の "Stop, and I say Go Go Go!!!!!"
と評価される.str
オペレーションは
(str "I say YES, You say NO, You say " "Stop, and I say Go Go Go!!!!!")
となり,2つの文字列が結合され, "I say YES, You say NO, You say Stop, and I say Go Go Go!!!!!" という新たな文字列が生成されて,(hello-goodbye :stop)
の評価結果として返される.
データ構造
Clojure ではすべてのデータ構造はイミュータブルだ.つまり,データ構造は基本的に変更できない.データ構造がミュータブル (mutable) な言語,例えば Ruby では,以下のようにフィールド上の変数を再代入するのはおなじみの方法だ.
section_9 = %[ "Aramaki" "Motoko" "Batou" ] section_9[0] = "Oyaji" section_9 #=> [ # "Oyaji" # "Motoko" # "Batou" # ]
ここではインデックスを0から始まる自然数で指定し,その値を上書きすることで配列を変更している.Ruby など,手続き型と呼ばれるプログラミングパラダイムを採用した言語では,データ構造はミュータブル,つまり変更可能なオブジェクトである場合が多い.実際,上の例でも,section_9
の0番目の値は"Aramaki" から "Oyaji" に変更され,変更前のリストは実質的に破棄されている.
Clojure には,こうしたオペレーションに相当するものはない.どうしてか,という説明はちょっと長くなりそうなので後回しにする.今のところは,細かいことを抜きにして,具体的なデータ構造をひとめぐりしよう.
nil
nil
は Clojure のどんなデータ型でも取りうる値だ.般若心経でいうところの空に相当する,'無' を表す記号.言語によっては null
と書かれることもあるが,実質はだいたい同じだ.制御フローのところでも言及したとおり,Clojure の条件分岐システムは nil
と false
をベースに設計されている.分岐条件のテストで偽 (logical-falsity) と評価されるのは nil
と false
だけ.他の値はすべて真 (logical-truth) となる.シーケンスプロトコル内での nil
はセンチネルとしても使われる.センチネル (sentinel) とは,可変長データの終了地点を示すための予約値のことなんだけど,ときどき待ち行列で見かける'最後尾プレート' くらいに思っておけばいい.
数値
Longs
自然数のこと.なんで自然数が Long と呼ばれるかというと,立ち入った話になるので省略.整数演算の結果がものすごく大きな数になった場合,java.lang.ArithmeticException という例外が投げられることがある.そんな時のために Clojure は +'
,-'
, *'
, inc'
,dec'
といった演算子を用意している.こうした" ' "付き演算子は,オーバーフロー時に BigInt という型に自動で変換されるが,通常の演算子に比べるとパフォーマンス面で劣る.
Ratio
整数同士の比を表現する,いわゆる有理数 ('有理数' が誤訳って話は有名だよね).Clojure では,結果が整数にならない割り算は,まず有理数として表現される.
(/ 3 7) ;=> 3/7
Contagion
コーディングの文脈では見慣れない表現だけど,Clojure で Contagion と言えば,大きな整数(BigInt) と 浮動小数点数 (float/double) のこと.この性質によって,処理内に BigInt を含む演算の結果はすべて BigInt になり,float や double を含む演算の結果はすべて double となる.Contagion は "伝染" くらいに訳される堅い単語だけど,まさに値の性質が演算全体に伝染するというわけ.
(* 3.0 2) ;=> 6.0 (/ 3.0 7) ;=> 0.4285714285714285
BigInt / BigDecimal
数値リテラルとしてBigInt には N,BigDecimal には M がそれぞれのお尻にくっつく.
(class 96786801248216348816N) ;=> clojure.lang.BigInt (class 3.14159265358M) ;=> java.math.BigDecimal
文字列
文字列は文字通り文字の列.他の多くの言語同様,文字列 (string) は文字のリストとして定義されている.一文字 (character) を明示的に扱いたい場合には,\a
\B
のように,各文字の直前に\
をつければいい.
以下のサンプルは,文字列操作用ライブラリであるclojure.string
内の upper-case
という関数を使っている.何をする関数かは,見ての通り.
(clojure.string/upper-case "serial experiments lain") ;=> "SERIAL EXPERIMENTS LAIN"
シーケンス
マップ
Clojure のマップは,他の言語でいう辞書や連想配列に相当するオブジェクトだ.ある値を別の値に紐づけて管理するための一般的な方法となる.そうそう,Clojure には map
という非常に (非常に!) よく使う関数があって,それと区別するためにデータ構造としてのマップをあえてここではカタカナで表記している.
Clojure は2種類のマップをサポートしているのだけど,ここではよりメジャーなハッシュマップに焦点を絞って説明する.では,具体的なマップリテラルをいくつか見てみよう.まずはこちら.
{}
空のマップだ.他の多くの言語同様,Clojure でもマップの表現に {}
を使う.
次に,よく見る形を紹介しておこう.
{:unit-01 "Shinji" :unit-00 "Rei"}
単語の頭に ' : ' がついたリテラルはキーワードと呼ばれるもので,Clojure の中でも特にピーキーな実装になっていて面白いんだけど,面白いものは説明が長くなるのであと回し.ここで大事なのは,マップが鍵 (key) と値 (value) のペアで構成されていることだ.鍵にはキーワードの他に,文字列も使える.ということで,def
を使って文字列を鍵にしたマップeva
を定義し,シンジくんが鍵としての役割から逃げないか確かめてみる.
(def eva {"Shinji" 1 "Rei" 0}) ;=> #'user/eva (eva "Shinji") ;=> 1
よくやったな,シンジ.
ちなみに,マップの値に関して特に制限はない.文字列だろうが,数値だろうが,ヴェクタやセットなどの他のシーケンスだろうが,なんでもありだ.関数だって使える.あんまり使わないけど.もちろん,マップはマップを値に取れる.つまり,ネスト構造も表現できるようになっている.これを利用して,eva-pilot
というマップを定義してみる.
(def eva-pilot {:unit-01 {:pilot "Shinji Ikari" :age 14 :character "nerd"} :unit-00 {:pilot "Rei Ayanami" :age 14 :character "quiet"}}) ;=> #'user/eva-pilot
マップリテラル {}
を使う代わりに,hash-map
関数でもマップを生成できる.評価結果が記入順と異なっているのには理由があるけれど,これまた立ち入った話なので今は気にしなくていい.以下の例では鍵-値のペアが見やすいよう,間に ' , ' を挿入した評価結果が返されるけれど,基本はあくまでもスペースを区切り記号として使う.
(hash-map :unit-01 "Shinji" :unit-00 "Rei") ;=> {:unit-00 "Rei", :unit-01 "Shinji"}
マップ内の値は get
を使って調べることができる.対応する鍵を見つけられなかった場合 get
は nil
を返す.
(get {:unit-00 "Rei" :unit-01 "Shinji"} :unit-00) ;=>"Rei" (get eva-pilot :unit-01) ;=> {:age 14, :pilot "Shinji Ikari", :character "nerd"} (get eva-pilot :unit-02) ;=> nil
ネストしたリスト内の値は get-in
とヴェクタを使って取得する.
(get-in eva-pilot [:unit-01 :age]) ;=> 14
この例だと,get-in
はまず :unit-01
を取得し,その内部の鍵 :age
の値を返している.ヴェクタ内のキーワードの順序はそのままネストの深さに対応しているので,ひとつの階層からはひとつの鍵しか取得できない.とはいえ,鍵を順序よく並べていくだけで任意の階層にアクセスできるのは非常に便利な性質だ.便利な性質といえば,もうひとつ,マップではキーワードを関数として使うことができる.
キーワード
ここまで何度か目にしているキーワードを,この辺りでしっかり説明しておこう.すでに了解されていることだろうが,キーワードはこんな見た目をしている.
:key-word :evangelion :22
これらキーワードはカッコの最初で関数として用いることで,データ構造の中で自分自身に対応する値を呼び出すことができる.
(:unit-02 {:unit-01 "Shinji" :unit-00 "Rei" :unit-02 "Asuka"}) ;=> "Asuka"
同じオペレーションは get
を使うとこう書ける.
(get {:unit-01 "Shinji" :unit-00 "Rei" :unit-02 "Asuka"} :unit-02) ;=> "Asuka"
こうしてみると,get
を使うよりもキーワード関数を使ったほうがより値に対して直接的で,書き方としても簡潔だ.とてもよく使うテクニックなので,今のうちに手に馴染ませておこう.
ヴェクタ
ヴェクタ (vector) は他の言語の配列 (array) に相当するもので,ヴェクタ内の各要素は0から始まる自然数をインデックスとして順序づけられている.ヴェクタはここまでの例に触れる中で何度か目にしているはず.マップと同様,ヴェクタもその要素になる値の種類に特別な条件はなく,どんなデータ型でも扱うことができる.
["serial" "experiments" "lain"] [3 2 1] [:a :b :c] [{:Ikari {:father "Gendou" :son "Shinji"}} :evangelion 0 1 2]
先述の通り,インデックス番号からヴェクタ内の各要素にアクセスできる.get
の出番だ.
(get ["serial" "experiments" "lain"] 2) ;=> lain (get [{:Ikari {:father "Gendou" :son "Shinji"}} :evangelion 0 1 2] 0) ;=>{:Ikari {:son "Shinji", :father "Gendou"}}
ヴェクタに新たな要素を追加するにはconj
を使う.このconj
という関数,パフォーマンス上の問題で追加先のデータ構造によって挙動に揺れがある.ヴェクタの場合,要素は最後尾に追加される.
(conj [1 2 3] 4) ;=> [1 2 3 4]
また,vector
という関数でもヴェクタを作ることができる.
(vector "serial" "experiments" "lain") ;=> ["serial" "experiments" "lain"]
リスト
ヴェクタによく似たデータ構造としてリスト (list) がある.似ている,ということは違いがあるということで,まずは見た目が違う.
'(1 2 3 4)
それと,リストに対しては get
が使えない.代わりに nth
という関数が用意されている.
(nth '(:a :b :c :d) 0) ;=> :a
list
関数でリストを作成することもできる.
(list 1 2 3 4) ;=> (1 2 3 4)
ここで注意.REPLでリストを評価すると,返ってくるリストは頭のクオートが消えて,見た目には関数呼び出しのオペレーションと区別がつかない.もちろん,Clojure はこの差をしっかりと認識してくれるが,初見の際にはちょっと驚くと思うので言及しておいた.クオートが反映されない理由もあるにはあるが,現段階では進行の障りになるので省略する.
さて,リストに対して conj
を使うと,要素はリストの先頭に追加される.
(conj '(1 2 3) 4) ;=> (4 1 2 3)
ここで問題がある.ヴェクタとリスト,お互いかなり似ているので,どうやって使い分けるかちょっと悩ましい.そこで,一つの指針を与えよう.つまり,先頭にすばやくデータを追加していきたい場合,それとマクロを書く場合にはリストを使う.それ以外では,ヴェクタを使った方が何かと便利な場合が多い.指針といってもこれは主観ではなくて,多くの Clojure プログラマが共有する知見だから安心して受け入れてほしい.
セット
セットはユニークな値のコレクションだ.ユニークな値っていうのはどういうことかというと,ひとつのセットの中には同じ値が存在しないということだ.そんなセットには2つの種類があるけれど,ここではよく目にするハッシュセットに絞って解説をする.
#{"Rei Ayanami" 14 :unit-00}
ハッシュセットはhash-set
を使って作ることもできる.ついでに,値のセットにおける値のユニークネスについても確認しておこう.
(hash-set "Rei" "Rei" "Asuka") ;=> #{"Rei" "Asuka"}
このように,セットは内部に複数の同一値を持てない.そう,代わりの綾波なんていないんだ.その証拠に,このセットに conj
で既存の値を追加しようとしても:
(conj #{"Rei" "Asuka"} "Rei") ;=> #{"Rei" "Asuka"}
と,素っ気なく無視される.
set
を使えば,既存のヴェクタやリストをセット化することができる.
(set ["Rei" "Rei" "Rei" "Rei" :unit-00]) ;=> #{:unit-00 "Rei"}
セット内に,ある値が存在するかどうかを調べるには contains?
を使う.そのものズバリな名前だ.名前の最後が '?' で終わっているので,ブール値が返されることも分かる.
(contains? #{"Rei" :unit-00} "Shinji") ;=> false (contains? #{"Rei" :unit-00} "Rei") ;=> true
無理やり同じ値をセットの中に詰めようとすると,シンタックスエラーが投げられてしまうので注意.
セットでも,キーワードを関数として自身の呼び出しに使うことができる.もしセット内に値があれば,の話だけれど.無かったら,例によってnil
が返される.
(:unit-00 #{"Rei" :unit-00}) ;=> :unit-00 (:unit-01 #{"Rei" :unit-00}) ;=> nil
セットにもget
が使える.
(get #{"Rei" :unit-00} :unit-00) ;=> :unit-00 (get #{"Rei" :unit-00} "Baka-Shinji") ;=> nil
けど,注意!セットには, nil
を含むかどうか確認するために get
を使うと,必ず nil
を返してしまうという厄介な性質がある.混乱するといけないから,代わりに contains?
を使うのが定石だ.
ここまで見てきた,マップ,ヴェクタ,リスト,セットなどのデータ構造は,まとめて 'シーケンス' と呼ばれる.Clojure にはこうしたシーケンスを自由自在に処理するための豊富な関数が用意されていて,実際,実践的な Clojure プログラミングの醍醐味はシーケンス処理をいかに使いこなすか,という点にある.と,言っても過言ではない.ここではシーケンスの実装と Java API に関する細かいことには立ち入らなかったが,気になる場合は clojure.org で逐次確認してみてほしい.