📝Clojure Idioms

📝Clojure Idioms

refs: 📂Clojure Core 🏷Clojure

Idiomsを駆使するとモテるとか.

Clojureの主にデータ操作に関する小技を書き溜めていく.

Ankiに突っ込むことで記憶もしていく.

Collections一般 #

リストにvalueを含むかという判定 #

少しやっかい. 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

nilのハンドリングまとめ #

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

andとorを条件分岐につかう #

orに与えられた式で真になるものが見つかったら残りを評価せずに真を返す(cf. yogthos/config).

andに与えられた式で偽になるものが見つからない限り残りを評価する.

ref: 📚Land of Lisp p47に書いてあった方法.

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

いわゆるデフォルト引数というものだが, 位置引数に値を設定する方法は見当たらない(見つけられてないだけかも).

代わりにオプション引数と キーワード引数 の文法の組み合わせでできる.

分配束縛(Destructuring) の or を活用する.

(defn myfunc
  [arg & {:keys [opt1 opt2] :or {opt1 "default1" opt2 "default2"}}]
  (format "arg=[%s] opt1=[%s] opt2=[%s]" arg opt1 opt2))

(myfunc "argument" {:opt1 "option1"})
;; => "arg=[argument] opt1=[option1] opt2=[default2]"

たんに変数がnilならば値を設定するならば or でいける.

(or input-argument "default")

みやすさで使い分けてもいい.

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))

nilならCollectionから取り除く #

(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を操作したい場合は以下のようにする.

(defn assoc-if
  [m key value]
  (if value (assoc m key value) m))

(-> m
  (cond-> value (assoc key value)))

🤔 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)

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})))

forをつかうと2つのコレクションの順列組み合わせになるので組み合わせ爆発する.

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

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)

リストを一定の個数ごとにまとめたい(list of list aka. 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))

平坦な(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)).

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

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で処理した結果は関数ごとのリストになるならば分配束縛によってそれぞれにリストに名前を束縛できる.

(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))))

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"}

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

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

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

似たような処理で2つのvectorからvector listを生成する方法も注意.

(map vector [:a :b :c] [:x :y :z])
;=> ([:a :x] [:b :y] [:c :z])

(reduce-kv f {} m) =>m : Mapのkey-valueに対してそれぞれ処理して合成 #

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}

集合について #

和集合 #

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})

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

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

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

References #