Effective Clojure/Arts of Clojure List Processing

Clojureの主にデータ操作に関する小技を書き溜めていく. Clojureベストプラクティス集.

できればEffective Clojure的な感じでもう少しBest Practiceを系統立てて整理したいところ(今はジャングル).

このへんのTipsやテクニックは自分で考えつくのは難しいので, みかけたら盗んで自分の道具箱にいれていく必要がある. イディオムを駆使するとモテるとか.

Collections一般

✅リストにvalueを含むかという判定はcontains?ではなくincludes?

少しやっかい. contains? は Mapにkeyが含まれるかを判定. また includes? は文字列のときにつかう.

リストに値が含まれるかはなんとcontains?では判定できない.

Javaの関数を使うのがいい.

(.contains [100 101 102] 101) => true

ref: data structures - Test whether a list contains a specific value in Clojure - Stack Overflow

listの要素の初めを取り出す(find-first)

filterしてfirstで取り出す. lazy sequenceなのでこれで十分.

(first (filter #(= % 1) '(3 4 1)))

ref. clojure - Return first item in a map/list/sequence that satisfies a predicate - Stack Overflow


forを使う方法もある.

(first (for [p pairs :when (= (:name p) "hoge")] p))

nilのハンドリングまとめ

もしnilなら(not-nil)なら…をまとめる(ref. 📝Clojure Logics).

✅Clojureでヌルポ例外どうするか問題では実際にヌルポ発生後の対処をまとめる.

or: 引数がnilならば別の値を設定

引数nilならば値を設定するならば or でいける.

(or input-argument "default")

cf. ✅関数の引数にデフォルト値を指定するには?

fnil: nilを 初期値で置き換えて関数適用

ref. fnil - clojure.core

データ操作をしようとしたときに, 引数でもらったデータ構造がnilの場合は初期値のデータ構造で置き換える.

(update request ::acc (fnil conj []) id)

これのやろうとしていることは, request mapの ::accというフィールドにあるvectorにidを追加しようとするが :acc がrequest mapにないときは空のvectorを用意してさらにidを加える.

条件つきifでスレッディングマクロ

identity をつかうことでスカしっぺできる. (identity x) はxをうけとってそれをそのまま返す.

 (let [third-step (if pred do-something identity)]
   (->> some-vec
        some-fn
        third-step
        further-processing))
;;;
 
(defn get
    [db & {:keys [queries] :or {queries identity}}]
    (-> db
        queries
        get))

もしnilでなければdo something, nilならばそのまま

もしnot-nilならばdo something, nilならばそのまま値を返したい.

whenをつかうとfalseのときにnilが戻るがこれは期待値ではない. ifを使うのは冗長. Threading Macrosのcond->をつかうときれいに書ける.

ref. cond-> 条件つきスレッドマクロ

(cond-> v
  (not (nil? v)) (hoge))

some->/some->> で副作用のある呼び出しを中断

to: some->/some->> : 副作用のある呼び出しを中断

nilならCollectionから取り除く

3つの方法がある.

  • (filter identity coll): falseとnilは除外される.
  • (filter some? coll)
  • (remove nil? coll)

map->map: Mapの単体変換まとめ

select-keys: あるMapからkeywordを指定してSubMapを作成

https://clojuredocs.org/clojure.core/select-keys

(select-keys {:a 1 :b 2} [:a])
;;=> {:a 1}

特定のkeywordsを削除してSubMapをするようなときはdissocをつかう. dissocには複数のkeywordを指定可能.

(dissoc {:a 1 :b 2 :c 3} :c :b)
;;=> {:a 1}

rename-keys: Mapのキーの名前変更

valueには触らずkeyだけ変更する. 名前は大事.

https://clojuredocs.org/clojure.set/rename-keys

clojuer.setに入っているので注意!(select-keys は clojure.coreなので間違えやすい. )

(clojure.set/rename-keys {:a 1 :b 2} {:a :new-a :b :new-b})
;; => {:new-a 1, :new-b 2}

Mapの要素でgroupingする

統計処理のgrouping相当は group-by で可能.

(group-by :tweet-id tweets)

Mapのキー(バリュー)に対して変換をしたい(関数をmapしたい)

reduce-kv をつかう.

ref. (reduce-kv f {} m) => m

変換のための関数は (fn [m k v]… )の引数にすること.

たとえば 値を変換したいならば (fn [m _ v] (… )) で関数を作成して変換したものをmにassocする.

条件付きMap操作: assoc-if/update-if 追加する値がnilでなければ操作

もしvalueがnilでなければMapを操作したい場合は以下のようにする.

;; https://stackoverflow.com/questions/16356888/assoc-if-in-clojure
 
(defn assoc-if
  ([m k v] (if v (assoc m k v) m))
  ([m k v f] (if v (assoc m k (f v)) m)))
 
(-> m
  (cond-> value (assoc key value)))
;; https://gist.github.com/pilku/f51eb30d4478a4015dec0bcb25bce0d2
 
(defn update-if
  ([m k f] (update m k #(if % (f %) nil)))
  ([m k f dv] (update m k #(if % (f %) dv))))

cond-> の記法がキレイ. ネストした構造は少しむずかしいかも. そもそもvalueを取り出さないと. いろいろと以下で議論されている. 汎用的に書くと難しいので個別にget-inした値を変数に保存してnil判定すればいいかも.

ref. dictionary - Clojure: idiomatic update a map’s value IF the key exists - Stack Overflow

nilの戻りを形容するならばsome->もありかな.

(defn- get-attr [node attr]
       (some-> node :attrs attr))

条件付きMap操作: Mapにvalueが存在しないならば追加

merge をつかうと左のMapと右のMapがあるときは右(後ろ)が優先される. もしvalueが存在するならばなにもしたくなければ右と左を入れ替えればいい.

ref. clojure assoc-if and assoc-if-new - Stack Overflow

ネストしたCollection操作にget-in/assoc-in/update-in

get-in/assoc-in/update-in をつかう.

https://clojuredocs.org/clojure.core/assoc-in

;; assoc-in into a nested map structure
(def foo {:user {:bar "baz"}})
(assoc-in foo [:user :id] "some-id")
;;=> {:user {:bar "baz", :id "some-id"}}

ref: 📝Clojure Map(clojure.core.map)

(条件付き)value変換(clojuer.walk/prewalk, clojure.walk/postwalk)

clojure.walkのprewalk/postwalkをつかうと深さ優先探索で再帰的にvalueを訪問することができる. prewalkは親ルートから, postwalkは枝から.

ここに変換関数を渡すことで条件にマッチした場合に変換を書けることができる.

(->> data
     (clojure.walk/prewalk #(cond->> %
                              (and (double? %) (> 0.01 %)) round4)))

ref. シリアライズできないマップにでくわした時 - iku000888-notes

key-aとkey-bのvalの計算結果をkey-cにbind

ひとつのkeyに対する変換ならupdateだか, 別のkeyを新しくkeyとしてbindするには?

普通にMapを受け取ってMapを返す関数を作成する.

list-of-maps

複数のシーケンスのそれぞれの要素からmapのシーケンスを作成

いわゆるPythonのzipのようなもの. indexを揃えつつ, コレクションを合成したい.

(map vector coll1 coll2)で2つのコレクションをくっつけたあと関数適用(分配束縛).

(->> (map vector [1 2 3] [4 5 6])
     (map (fn [[x y]] {:a x :b y})))

Mapのリストをあるkeyを元にひとつのMapに集約

  • group-byをつかう.
  • zipmapをつかう. (zipmap (map :name opts) opts))
  • reduceをつかう. group-byのような挙動をさせるにはconjでの結合が便利
(reduce (fn [group {:keys [pair-key] :as pool}]
          (let [acc (get group pair-key)]
            (assoc group pair-key (conj acc pool)))) {})

idをもつMapのlistを1つのMapに変換

(->> coll
     (map (juxt #(:id %) identity))
     (into {}))

list of mapsから特定のidをもつmapを検索

事前にlist of mapsを一つのmapにreduceしてから検索する方法とfilter関数を使って直接検索する方法がある.

  • 事前にMapにreduce
    • 検索のオーダーはO(1).
    • 変換処理に時間とメモリがかかる.
  • filter
    • 検索オーダーはO(n)
    • 省メモリ.
    • lazy sequenceなのでそこまで遅くない(idのみチェック).
    • そこまでミッションクリティカルでなければfilterで十分.

list-of-lists

ネストしたリスト(可変長引数)に関数を適用したい

関数の 可変長引数関数 対する関数適用で使えるテクニック.

(defn hello [greeting & who]
  (println greeting who))

greetingとwho(可変)に関数を適用するには apply をつかう.

以下は同じ.

(apply + 1 2 3 [4 5])
(apply + [1 2 3 4 5])
(apply + 1 2 3 4 5)

リストを一定の個数ごとにまとめたい(partition: list of list, chunked list)

clojure.core.partition を利用するとリストを指定した個数ごとに分割できる.

n個のシーケンスをm個ずつのシーケンスに分割することができる, いわゆるチャンクができる.

(partition 4 (range 20))
;;=> ((0 1 2 3) (4 5 6 7) (8 9 10 11) (12 13 14 15) (16 17 18 19))

チャンクの最後できれいに割り切れない場合は切れ捨てられるところが怒りポイント. この場合は partition-all という別の関数が用意されている.

(partition 4 (range 18))
;; => ((0 1 2 3) (4 5 6 7) (8 9 10 11) (12 13 14 15))
 
(partition-all 4 (range 18))
;; => ((0 1 2 3) (4 5 6 7) (8 9 10 11) (12 13 14 15) (16 17))

see also. Clojure: Sliding window Algorithm

✅平坦なシーケンスのテクニック(flatten)

平坦なとはflatten という英語でよく登場する.

Clojureの関数もある => flatten - clojure.core ClojureDocs

ネストした構造やassociateveな構造をシーケンシャルに処理したいときにつかう. といいつつ自力てうまいてを思いつくのもコツが必要なので結局idiomを覚えていくのがいい. ということでここにまとめる.

Map => flatten sequence => Map

apply & concatでflattenなシーケンスに変換. flattenを直接つかうとネストした二階層目以降も全てフラットにしてしまう.

(apply concat {:a "foo" :b "bar"})
;;=> (:a "foo" :b "bar")

Mapに戻すのは into {} (map (juxt identity f)).

list of list of listsをlist of listにreduceする

単純にflattenをつかうとすべてを単一のlistにしてしまう.

とくにfor文でつかう.

 
(def coll [[[1 2 3] [4 5 6]] [[1 2 3] [4 5 6]]])
 
;; forをつかったあとにもとのリストに戻す
(defn- unpack-for [xs] (reduce (fn [acc x] (apply conj acc x)) [] xs))

シーケンスの分離と合流テクニック

map, filter, reduceはシーケンスに対して一つの関数を適用していく. しかし時には, シーケンスに対して別々の関数をそれぞれ適用することでシーケンスを分離したい, また最終的には分離したシーケンスをそれぞれ処理したあと合流させたい.

非同期処理だと pipeline があるが, ここではそこまでは踏み込まない.

複数の関数を一つの値に適用(juxt)

juxtが分離のためのよい関数として使える. juxtapositionの略, 日本語訳だと並列らしい.

複数の関数を受取り, それらの関数を一つの値に適用したvectorを返す. まさに分離のための関数だ. ((juxt a b c) x) => [(a x) (b x) (c x)].

juxt - clojure.core | ClojureDocs

(juxt f g z.. )のみだと単なる関数なので, この関数を他と組み合わせていくテクニックも学ぶ必要がある.

たとえばmapとidentityを使えば元の値を保持しつつ別の変化も並列で保持できる. 変換前, 変換後みたいな.

((juxt identity name) :keyword)
;;=> [:keyword "keyword"]

juxt と分配束縛の合わせ技

さらにjuxtで処理した結果は関数ごとのリストになるならば分配束縛によってそれぞれにリストに名前を束縛できる.

(let [[even-numbers odd-numbers :as result]
      ((juxt filter remove) even? numbers)]
  result)

もしくはmapの途中でjuxtした次のステップで無名関数の引数で分配束縛を使ったり. 便利だ!

(->> colls
     (map (juxt #(->a %) #(->b %)))
     (map (fn [[[[a b]]]] (proc a b))))

ref. 📝Clojure: 分配束縛 - Destructuring

into {} (map (juxt identity f)): flattern vectorをMapに変換

2つのシーケンスというよりは, 一つのシーケンスの中にkey valueが交互に現れるようなものをmapに変換する.

juxtをつかうとkey valueの順で分離したシーケンスが生成されるのでそれらをMapに合流させるようにつかう.

(juxt identity name)
 
;; 上は以下と同じ.
(fn [x] [(identity x) (name x)])
 
;; よってjuxtと組み合わせればMapにできる.
(into {} (map (juxt identity name) [:a :b :c :d]))
;;=> {:a "a" :b "b" :c "c" :d "d"}

✅複数のシーケンスからMap生成(zipmap v1 v2) =>m

2つのシーケンスからMapを生成する.

(zipmap [:a :b :c :d :e] [1 2 3 4 5])
;;=> {:a 1, :b 2, :c 3, :d 4, :e 5}

APIのrequest-paramsをzipmapで作成

let内で用意したデータをzipmapをつかってmapのvalueに格納するテクニック.

(defn get-market-ohlc-by-date [exchange pair periods date]
  (let [path        (str "/markets/" exchange "/" pair "/ohlc")
        url         (str end-point path)
        [start end] (time/get-start-end-unixtime date)
        params      (zipmap [:periods :after :before] [periods start end])]
    (-> (http/get url {:params params})
        :body
        :result)))

✅複数のリストを1つのリストに合流(zip)

Pythonでいうzip的な処理. Clojureでは組込み関数はないので, map/vectorやinterleave/partition関数で同時に処理したいデータの集合を作成してそれに関数を当てる.

(map vector '(1 2 3) '(4 5 6))
;; => ([1 4] [2 5] [3 6])
(map vector [:a :b :c] [:x :y :z])
;=> ([:a :x] [:b :y] [:c :z])
(partition 2 (interleave '(1 2 3) '(4 5 6)))
;; => ((1 4) (2 5) (3 6))

それぞれのデータを関数の変数に割り当てるには分配束縛(Destructuring)がいい.

(->>
 (interleave '(1 2 3) '(4 5 6))
 (partition 2)
 (map (fn [[x y]] (+ x y))))

このテクニックは知らないと自力で考えるのはきつい…

✅Mapのkey-valueに対してそれぞれ処理して合成(reduce-kv)

Mapをシーケンスとして扱うユーティリティだが, keyに対する処理, valueに対する処理, そしてkeyとvalueを合わせた処理など, いろいろできる.

%1=map, %2=key, %3=valがbindされる.

;; keyとvalueを入れ替え
(reduce-kv #(assoc %1 %3 %2) {} {:a 1 :b 2 :c 3})
;;=> {1 :a, 2 :b, 3 :c}
 
;; valueを2倍の数値に修正
(reduce-kv #(assoc %1 %2 (* 2 %3)) {} {:a 1 :b 2 :c 3})
;;=> {:a 2, :b 4, :c 6}

keyに対するvalueに対して処理をしてまたkeyにassoc

このパターンもよくつかう.

(reduce-kv (fn [m k v] (assoc m k (count v))) {} coll)

集合について

和集合

clojure.set/union が使える.

(require '[clojure.set :refer [union]])
 
(union #{1 2 3} #{3 4 5})

リストの場合は2つをconcatでくっつけてからdistinctで一意にする.

user> (distinct (concat '(1 2 3) '(2 3 4)))
=> (1 2 3 4)

intoを使った合わせ技もある.

(into '() (into #{} (clojure.set/union '(1,2,3) '(3,4,5))))

差集合

clojure.set/difference をつかう.

(clojure.set/difference #{1 2 3} #{3 4 5})

順列/組合せ

forをつかった組合せ

forをつかう.

(for [x [1 2 3] y [4 5 6]] (vector x y))
 
;; ([1 4] [1 5] [1 6] [2 4] [2 5] [2 6] [3 4] [3 5] [3 6])

doseqをつかうとnilがreturnされてmapと組み合わせられない.

手続き処理の中で処理をsleep

JavaのThreadをつかう. msで指定.

(Thread/sleep 5000)
  • 以下は便利なhelper function(ref).
(defn doseq-interval
  [f coll interval]
  (doseq [x coll]
    (Thread/sleep interval)
    (f x)))

atomとdelayのコンボでの副作用管理

Atoms, delays and side effects: a resource management idiom for Clojure

外部とやり取りするような副作用の処理はうっかり実行してしまうと処理が重いので delay をつかう. そのような重い処理が多重実行しないように, atom で値に格納してswap!で取り替える.

(def state (atom (delay xxx)))
(defn refresh-state [] (swap! @state fn-))

たいていの場合は状態管理ライブラリがよろしくやってくれいてるが, スクラッチでいろいろ書く時に必要となる小技.

References

up: 📂Clojure Core