ハツカネズミの恋

Lisp のもろもろ,おぼえがき

Clojure ことはじめ① - 導入 / データ構造 -

Clojureについて

ClojureJVM をプラットフォームとして動作するLispだ.Clojure というイカした名前にはいくつかの由来がある.ひとつめは,同音異義語である 'closure'.日本語でいうと閉包.この言葉,数学では位相空間論で登場するが,プログラミングの文脈では ’自由変数を関数内に閉じ込める役割を持った無名関数’のことで,高階関数の引数としておなじみ.まあ,今のところはそんなに気にしなくていい.ふたつめは,Common LispScheme と並んで広く浸透している Lisp のひとつ.Lisp には名前空間の数に対応して,Lisp-1, Lisp-2 の二種類があり,SchemeLisp-2 ,Common LispLisp-1,ClojureLisp-1 だ.名前空間とは何か,そもそもLisp って何か,という話はひとまず置いておこう.Clojure という名前について,Common Lisp からは 'C' と 'L' を拝借している.みっつめは,Java.お察しの通り,これは Clojure の 'J' にあたる.先述の通り,ClojureJVM (Java Virtual Machine) を言語プラットフォームとして採用していて,この点で他の Lisp とは異質だ.というのは,ほとんどの Lisp はそれぞれ独自のプラットフォームを持っている.Clojure を '実用的な' Lisp と評する向きはおおむねこの,JVM 上で動作する,という点に着目してのことだろうと思う.ClojureJava の蜜月はかなりのもので,詳しくは後述するが,Java を自在に操作することは,必須とは言わないまでも,目下の問題に対する Clojure によるアプローチをより豊かなものにするだろう.ひとつめの 'closure' はダジャレ的な意味合いが強いものの,(Common) LispJavaClojure という言語の設計や思想に直接的な影響を与えている.ではその設計や思想とはなんぞや,という話になるが,僕自身そこまで深く理解している自信がないので,おいおいまとめようと思う.また,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"

原則的にと書いたのは,視認性のため ' , ' を使う場合もあるからだ.これは後ほど紹介する.ここでは簡単に上の例を説明しよう.まあ,最初の二つに関しては,説明するまでもなく一目瞭然だろうけど.最初の例では + というオペレータが 12 というオペランドに適用されて,3 と評価されている.'適用(application)' とか '評価(evaluation)' とかいう言い回しは関数型プログラミングに特徴的なものだけど,さしあたりそこまで気にする必要はない.

制御フロー

ここでは,ifdowhen という3つの制御フローを紹介する.

if

まず,if フォームの基本的な形はこんな感じ.

(if test-form
  then-branch
  else-branch)

Clojureif オペレータを見つけると,直後のtest-form をまず評価する.test-form はブール値として truefalse に評価される.test-formtrue と評価された場合 Clojurethen-branch を評価し,false の場合は else-branch を評価する. この辺り,Clojure の真偽判定はちょっと特殊で,nilfalse だけが偽,その他の場合はすべて真となる.

(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

ご覧のとおり,Clojureifオペランドの配置順序が分岐先に関連付けられているので,then-branchelse-branch も,それぞれひとつのフォームしか持つことができない.これは評価結果の一意性という点ではすごく嬉しいことなのだけど,実際,ちょっと不便だ.そんな不便を解消するために,Clojuredo というオペレータを用意している.

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" という文字列が返されている.お察しのとおり,ifdo はしばしば使われる組み合わせだ.そこで,このふたつの機能をひとつにまとめたものが存在する.それが when だ.

when

whenelse-branch はない.英語における when という関係詞の用法から考えても,これは自然な振る舞いだ. whentest-formtrue に評価されると,以降のフォームを順次実行する.test-formfalse の場合は 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 のブールオペレータとして andorがある.or はオペレーション内で最初に真と評価された値,もしくは最後の値を返す.and は最初に偽と評価された値を返すが,もしオペレーション内に偽となる値がなかった場合,最後に真と評価された値を返す.と言われてもなんのこっちゃ,という感じだと思うので,具体例を見てみよう.まずは or から.

(or nil false :the-first-truthy-value "the second truthy value")
;=> :the-first-truthy-value

(or (= 1 2) (= "fooly" "cooly"))
;=> false

最初の例で,nilfalse は両方とも偽となる値だ.そこで or はオペレーション内で最初に真と評価された(つまり nil でも false でもない) 値である:the-first-truthy-value を返した.次の例では,(= 1 2)false(= "fooly" "cooly")false なので,全体は (or false false) となる.false は偽なので,このオペレーション内のオペランドはすべて偽と評価されたことになる.ということで,最後尾の値である falseor オペレーション全体の評価結果として返されている.

続いて,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 の値によって分岐が決定され,分岐の決定がそのまま,返される文字列に対応する. 試しに :stopyou-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

nilClojure のどんなデータ型でも取りうる値だ.般若心経でいうところの空に相当する,'無' を表す記号.言語によっては null と書かれることもあるが,実質はだいたい同じだ.制御フローのところでも言及したとおり,Clojure の条件分岐システムは nilfalse をベースに設計されている.分岐条件のテストで偽 (logical-falsity) と評価されるのは nilfalse だけ.他の値はすべて真 (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 を使って調べることができる.対応する鍵を見つけられなかった場合 getnil を返す.

(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 で逐次確認してみてほしい.