Clojureに入門したら知っておきたいN個のこと
Clojure入門者が早めに知っておくと良いかもしれない+αの知識について書きまとめてみます。
前提知識としては
Clojure公式サイトのチュートリアル
Clojure - Learn Clojure
Clojure - Programming at the REPL
Programming Clojure /『プログラミングClojure』
Living Clojure
などのチュートリアルや入門書を読んでClojureの概要は把握していることをおよそ想定しています。
より詳しい情報源についてはClojure/ClojureScript関連リンク集なども適宜参考にしてみてください。
また、この記事(メモ)のアイディアをきっかけに執筆した『Clojureに入門したら知っておきたい5つのこと』という同人誌もあります。
データ構造とシーケンスの関係を理解する
Clojureのデータ構造は抽象をもとに設計されているため、どのような抽象があるのか把握しておくことは重要です。
以下のようなコードの評価結果を見てみましょう。
code:clojure
;; conj
user> (conj '(1 2 3) 4) ; リスト
(4 1 2 3)
user> (conj 1 2 3 4) ; ベクター
1 2 3 4
user> (conj #{1 2 3} 4) ; セット
#{1 4 3 2}
;; 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)
(select-keys :a :b))
{:a 1, :b 8} ; 結果はマップ
;; ベクターにシーケンス操作関数を適用
user> (->> 1 2 3
(filter odd?)
(map #(* % %)))
(1 9) ; 結果はシーケンス
コレクションとシーケンスに関する主な抽象(インターフェース)には以下のものがあります。
リスト(list)
clojure.lang.IPersistentList インターフェースを実装したもの
clojure.lang.PersistentList
clojure.lang.PersistentQueue: キュー(queue)
list? で判定
ベクター(vector)
clojure.lang.IPersistentVector インターフェースを実装したもの
clojure.lang.PersistentVector
clojure.lang.MapEntry: マップエントリー(マップの要素)
vector? で判定
セット(set)
clojure.lang.IPersistentSet インターフェースを実装したもの
clojure.lang.PersistentHashSet
clojure.lang.PersistentTreeSet
set? で判定
マップ(map)
clojure.lang.IPersistentMap インターフェースを実装したもの
clojure.lang.PersistentArrayMap
clojure.lang.PersistentHashMap
clojure.lang.PersistentTreeMap
map? で判定
コレクション(collection)
clojure.lang.IPersistentCollection インターフェースを実装したもの
リスト
ベクター
セット
マップ
シーケンス
coll? で判定
シーケンス(sequence)
clojure.lang.ISeq インターフェースを実装したもの
clojure.lang.PersistentList
seq 関数でシーケンス化したもの
seq? で判定
シーカブル(seqable)
seq 関数がサポートされている(= シーケンス化可能な)もの
clojure.lang.ISeq ※すでにシーケンスの場合
clojure.lang.Seqable
nil
Iterable
配列
CharSequence ※文字列(String)など
java.util.Map
seqable? で判定
code:clojure
user> (let [fs #'identity #'type #'list? #'vector? #'set? #'map? #'coll? #'seq? #'seqable?
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)) ; キューのシーケンス
1 2 3 ; ベクター
(seq 1 2 3) ; ベクターのシーケンス
#{1 2 3} ; セット
(seq #{1 2 3}) ; セットのシーケンス
{:a 1 :b 2 :c 3} ; マップ
(seq {:a 1 :b 2 :c 3}) ; マップのシーケンス
"abc" ; 文字列
(seq "abc") ; 文字列のシーケンス
(long-array 1 2 3) ; 配列
(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
Clojure - Frequently Asked Questions - Collections, Sequences, and Transducers
Clojure - Data Structures
Clojure - Sequences
Sequences - Inside Clojure
Clojureにおけるデータ構造の抽象化を理解して独自のデータ構造を実装する Part 1: Collectionとは何か
Clojureにおけるデータ構造の抽象化を理解して独自のデータ構造を実装する Part 2: Sequenceとは何か
Clojure Applied
Collect and Organize Your Data
名前空間とVarについて理解する
;; TODO: 解説を書く
名前空間(Namespace)、 Var 、シンボル(Symbol)、キーワード(Keyword)
Further Reading
Clojure - Evaluation
Clojure - Namespaces
Clojure - Vars and the Global Environment
def と Symbol と Var の話 - (-> % read write unlearn)
The Relationship Between Clojure Functions, Symbols, Vars, and Namespaces | 8th Light
Why does Clojure distinguish between symbols and vars? - Stack Overflow
マップとレコードを適切に使い分ける
;; TODO: 解説を書く
ドメインのエンティティ(データモデル)を表現するには、まずはマップを利用し、プロトコルによるポリモーフィズムが必要な場合や固定のフィールドを持つデータを効率的に扱いたい場合などにレコードの利用を考えるのがオススメかも?
cf. オブジェクト指向プログラミング
e.g. Ring, clojure.java.jdbc (外部とのインターフェースとしてマップを使う例)
Further Reading
When to use map vs defrecord? - Clojure - PurelyFunctional.tv Discussions
Clojure - Datatypes: deftype, defrecord and reify
日本語版: Clojure - データ型: deftypeとdefrecordとreify
プロトコルによる依存関係逆転を利用する
;; TODO: 解説を書く
外部との境界面でプロトコルを利用することで疎結合になる話とか?
cf. Duct
Further Reading
Clojure - Protocols
Boundaries · duct-framework/duct Wiki
データ > 関数 > マクロの順に考える
;; TODO: 解説を書く
Clojureコミュニティでは"data > functions > macros"だと言われることがあります。
データ指向なDSL
e.g. Honey SQL (SQL), Reagent (HTML), Garden (CSS)
マクロ
e.g. core.async, core.match
Further Reading
Data > Functions > Macros. But why? - LispCast
clojure.specを活用する
;; TODO: 解説を書く
近年、良い型システムを備えた静的型付け(static typing)の言語の評価が高まり、動的型付け(dynamic typing)の言語からも漸進的型付け(gradual typing)を志向するものが支持される傾向があるようです。
動的言語であるClojureにおいても過去にcore.typedという漸進的型付けを実現する準標準ライブラリが注目されたこともありました。
しかし現在はClojure 1.9で標準ライブラリに追加されたclojure.specを活用するのが主流になりつつあります。
cf. Racketのcontract system
cf. schema
Further Reading
Clojure - clojure.spec - Rationale and Overview
日本語版: Clojure - clojure.spec - 論理的根拠と概要
Clojure - spec Guide
エディタとREPLを連携させる
;; TODO: 解説を書く
e.g. CIDER, Cursive, fireplace.vim, Proto REPL, Calva
cf. rebel-readline
user名前空間とclojure.tools.namespaceを活用する
;; TODO: 解説を書く
clojure.tools.namespace
アプリケーション状態/ライフサイクル管理ライブラリを利用する
;; TODO: 解説を書く
Component, mount, Integrant
cf. Duct
Further Reading
Clojure Workflow Reloaded
S式の構造的編集に親しむ
;; TODO: 解説を書く
ParEdit, Smartparens, Parinfer
イディオマティックなコードを書く
;; TODO: 解説を書く
The Clojure Style Guide
e.g. cljfmt, eastwood, kibit
依存ライブラリをチェックする
;; TODO: 解説を書く
Clojureコミュニティでは一般に、多機能のライブラリや「フレームワーク」よりも単機能に特化した小さなライブラリの組み合わせが好まれる傾向があり、言語自体も後方互換性を非常に重視していることから、成熟したライブラリはたとえ数年以上の更新がなかったとしても安心して利用可能であることがほとんどです。
プロジェクトの依存ライブラリのバージョン更新が自動的にチェックできると便利です。
Leiningenでは、lein-ancient
依存ライブラリのバージョン競合を検出するには lein deps :tree
#Clojure