Clojureに入門したら知っておきたいN個のこと
Clojure入門者が早めに知っておくと良いかもしれない+αの知識について書きまとめてみます。
前提知識としては
Clojure公式サイトのチュートリアル
などのチュートリアルや入門書を読んでClojureの概要は把握していることをおよそ想定しています。
データ構造とシーケンスの関係を理解する
Clojureのデータ構造は抽象をもとに設計されているため、どのような抽象があるのか把握しておくことは重要です。
以下のようなコードの評価結果を見てみましょう。
code:clojure
;; conj
user> (conj '(1 2 3) 4) ; リスト
(4 1 2 3)
user> (conj 1 2 3 4) ; ベクター user> (conj #{1 2 3} 4) ; セット ;; cons
user> (cons 4 '(1 2 3)) ; リスト
(4 1 2 3)
user> (cons 4 1 2 3) ; ベクター (4 1 2 3)
user> (cons 4 #{1 2 3}) ; セット (4 1 3 2)
;; map
user> (map inc '(1 2 3)) ; リスト
(2 3 4)
user> (map inc 1 2 3) ; ベクター (2 3 4)
user> (map inc #{1 2 3}) ; セット (2 4 3)
リスト、ベクター、セットに関数 conj を適用した結果は要素が追加された元と同じ種類のコレクションです。
一方、関数 cons や map を適用した結果は何でしょう?
いずれも丸括弧で括られて表示されているのでリストでしょうか?
正しくはリストではなく「シーケンス」(sequence)です。
シーケンスとは first, rest, cons という基本操作をサポートするインターフェース(clojure.lang.ISeq)であり、「論理的なリスト」(データの連なり)を表す抽象です。
Clojureではコレクションに限らず様々なデータ構造がシーケンス化可能(seqable)であり、シーケンスという共通の抽象を基礎とした豊富なシーケンス操作関数(e.g. map, filter, take, ...)で統一的に扱うことができます。
具体的なデータ構造としてのリスト(clojure.lang.PersistentList)は clojure.lang.ISeq を実装しているため同時にシーケンスでもあり、REPLで丸括弧で表示されることから少々紛らわしいですが、リストならばシーケンスですがシーケンスならばリストだとは限りません。
ベクター(clojure.lang.PersistentVector など)やセット(clojure.lang.PersistentHashSet など)はリストと同じくコレクション(clojure.lang.IPersistentCollection)の一種ですが、それ自体としてシーケンス(clojure.lang.ISeq)ではなくあくまでシーカブル(clojure.lang.Seqable)であり、シーケンスを操作する関数の適用時にシーケンス化されることでシーケンスが得られます。
標準ライブラリでは原則的に、データ構造を受け取って同種のデータ構造を返す関数は対象のデータを第1引数でとり、シーケンス化可能なデータ構造を受け取ってシーケンスを返す関数は対象のデータを最終引数でとるように設計されています。
このことから、ある関数が元と同じデータ構造を返すのかシーケンスを返すのかを関数のシグネチャで区別でき、前者の関数は -> (thread-first)系のマクロ、後者の関数は ->> (thread-last)系のマクロと組み合わせて使いやすくなっています。
code:clojure
;; マップにマップ(データ構造)操作関数を適用
user> (-> {:a 1 :b 2 :c 3}
(update :b * 4)
{:a 1, :b 8} ; 結果はマップ
;; ベクターにシーケンス操作関数を適用
(filter odd?)
(1 9) ; 結果はシーケンス
コレクションとシーケンスに関する主な抽象(インターフェース)には以下のものがあります。
リスト(list)
list? で判定
ベクター(vector)
vector? で判定
セット(set)
set? で判定
マップ(map)
map? で判定
コレクション(collection)
リスト
ベクター
セット
マップ
シーケンス
coll? で判定
シーケンス(sequence)
clojure.lang.PersistentList
seq 関数でシーケンス化したもの
seq? で判定
シーカブル(seqable)
seq 関数がサポートされている(= シーケンス化可能な)もの
clojure.lang.ISeq ※すでにシーケンスの場合
nil
Iterable
配列
CharSequence ※文字列(String)など
java.util.Map
seqable? で判定
code:clojure
fnames (map (comp :name meta) fs)
inspect #(zipmap fnames ((apply juxt fs) %)) xs ['(1 2 3) ; リスト
(seq '(1 2 3)) ; リストのシーケンス ※元のリストと同一
(conj clojure.lang.PersistentQueue/EMPTY 1 2 3) ; キュー
(seq (conj clojure.lang.PersistentQueue/EMPTY 1 2 3)) ; キューのシーケンス
(seq #{1 2 3}) ; セットのシーケンス {:a 1 :b 2 :c 3} ; マップ
(seq {:a 1 :b 2 :c 3}) ; マップのシーケンス
"abc" ; 文字列
(seq "abc") ; 文字列のシーケンス
(seq (long-array 1 2 3)) ; 配列のシーケンス ]]
(clojure.pprint/print-table fnames (map inspect xs)))
| identity | type | list? | vector? | set? | map? | coll? | seq? | seqable? |
|-----------------------------------+------------------------------------------------+-------+---------+-------+-------+-------+-------+----------|
| (1 2 3) | class clojure.lang.PersistentList | true | false | false | false | true | true | true |
| (1 2 3) | class clojure.lang.PersistentList | true | false | false | false | true | true | true |
| clojure.lang.PersistentQueue@7861 | class clojure.lang.PersistentQueue | true | false | false | false | true | false | true |
| (1 2 3) | class clojure.lang.PersistentQueue$Seq | false | false | false | false | true | true | true |
| 1 2 3 | class clojure.lang.PersistentVector | false | true | false | false | true | false | true | | (1 2 3) | class clojure.lang.PersistentVector$ChunkedSeq | false | false | false | false | true | true | true |
| #{1 3 2} | class clojure.lang.PersistentHashSet | false | false | true | false | true | false | true | | (1 3 2) | class clojure.lang.APersistentMap$KeySeq | false | false | false | false | true | true | true |
| {:a 1, :b 2, :c 3} | class clojure.lang.PersistentArrayMap | false | false | false | true | true | false | true |
| (:a 1 :b 2 :c 3) | class clojure.lang.PersistentArrayMap$Seq | false | false | false | false | true | true | true | | abc | class java.lang.String | false | false | false | false | false | false | true |
| (\a \b \c) | class clojure.lang.StringSeq | false | false | false | false | true | true | true |
| [J@6151c015 | class [J | false | false | false | false | false | false | true |
| (1 2 3) | class clojure.lang.ArraySeq$ArraySeq_long | false | false | false | false | true | true | true |
nil
Further Reading
Collect and Organize Your Data
名前空間とVarについて理解する
;; TODO: 解説を書く
名前空間(Namespace)、 Var 、シンボル(Symbol)、キーワード(Keyword)
Further Reading
マップとレコードを適切に使い分ける
;; TODO: 解説を書く
ドメインのエンティティ(データモデル)を表現するには、まずはマップを利用し、プロトコルによるポリモーフィズムが必要な場合や固定のフィールドを持つデータを効率的に扱いたい場合などにレコードの利用を考えるのがオススメかも?
cf. オブジェクト指向プログラミング
Further Reading
プロトコルによる依存関係逆転を利用する
;; TODO: 解説を書く
外部との境界面でプロトコルを利用することで疎結合になる話とか?
Further Reading
データ > 関数 > マクロの順に考える
;; TODO: 解説を書く
Clojureコミュニティでは"data > functions > macros"だと言われることがあります。
データ指向なDSL
マクロ
Further Reading
clojure.specを活用する
;; TODO: 解説を書く
近年、良い型システムを備えた静的型付け(static typing)の言語の評価が高まり、動的型付け(dynamic typing)の言語からも漸進的型付け(gradual typing)を志向するものが支持される傾向があるようです。
動的言語であるClojureにおいても過去にcore.typedという漸進的型付けを実現する準標準ライブラリが注目されたこともありました。 しかし現在はClojure 1.9で標準ライブラリに追加されたclojure.specを活用するのが主流になりつつあります。 cf. Racketのcontract system
Further Reading
エディタとREPLを連携させる
;; TODO: 解説を書く
user名前空間とclojure.tools.namespaceを活用する
;; TODO: 解説を書く
アプリケーション状態/ライフサイクル管理ライブラリを利用する
;; TODO: 解説を書く
Further Reading
S式の構造的編集に親しむ
;; TODO: 解説を書く
イディオマティックなコードを書く
;; TODO: 解説を書く
依存ライブラリをチェックする
;; TODO: 解説を書く
Clojureコミュニティでは一般に、多機能のライブラリや「フレームワーク」よりも単機能に特化した小さなライブラリの組み合わせが好まれる傾向があり、言語自体も後方互換性を非常に重視していることから、成熟したライブラリはたとえ数年以上の更新がなかったとしても安心して利用可能であることがほとんどです。
プロジェクトの依存ライブラリのバージョン更新が自動的にチェックできると便利です。
依存ライブラリのバージョン競合を検出するには lein deps :tree