Clojureは関数型プログラミング言語らしくリスト操作の関数が揃っているのですが、
clojure.core - ClojureDocsに雑多に積まれて居て探すのに一苦労します。
Clojureの関数はコレクションが第一引数・最終引数がバラバラで完全に覚えるまで大変なので、
Lodashのドキュメントのようにカテゴリ別に頻出のリスト操作関数を備忘録としてまとめていきます。
(基本的にはClojureDocsを意訳した感じにする予定でリンクも張っていきます、リンク先も確認するようにしてください。)
MapのようにSequenceで使った時と、Mapで使った時に期待される動作が違うケースがありますので、今回は同じmap関数でもそれぞれで紹介するというやり方にしています。
Sequence
引数1個
first
(first coll)
シーケンスの先頭を返す関数です。
clojureは他の言語と違い、find関数が条件に一致する先頭要素を抜き出す関数ではないので、filterとfirstによって抜き出すことが多いです。
(first [1 2])
;;=> 1
;; filter関数と併用
(->> [{:name "taro" :age 20} {:name "jiro" :age 18}]
(filter #(= (:age %) 18))
first)
;; {:name "jiro", :age 18}
last
(last coll)
firstの逆でシーケンスから最後の要素を抜き出す関数です。
(last ["a" "b" "c" "d" "e"])
;;=> "e"
-> thread-first
conj
(conj coll x)
(conj coll x & xs)
conjoiningの略称でコレクションの結合を行います。
他の言語やライブラリではpush、concat、append等で表現される事もあります。
ベクターとそれ以外で挙動が変化するようなので要確認。
;; ベクターの場合は末尾に追加したベクターを返します
(conj [1 2 3] 4)
;;=> [1 2 3 4]
;; リストの場合は先頭に追加したリストを返します
(conj '(1 2 3) 4)
;;=> (4 1 2 3)
;; 複数要素の追加にも対応しています
(conj [1 2] 3 4)
;;=> [1 2 3 4]
(conj '(1 2) 3 4)
;;=> (4 3 1 2)
string/split
(split s re)
(split s re limit)
coreには含まれず、clojure.stringに含まれる頻出関数です。
文字列を砕いて配列にします。
シーケンスを扱う関数の殆どは第二引数にベースとなる値を利用しますが、string/splitは第一引数に対象の文字列を入れる事に注意してください。
ただし、Clojure流儀に従うならスレッディングマクロで型をコロコロ変換する使い方は避けた方が良いみたいなので、一度シーケンスにしてから->>
を使う手法が一般的になりそうです。
(require '[clojure.string :as string])
(string/split "Clojure is awesome!" #" ")
;;=> ["Clojure" "is" "awesome!"]
;; CSVからMapを作る
(->> (string/split "1,taro,18" #",")
(zipmap [:id :name :age]))
;;=> {:id "1", :name "taro", :age "18"}
->> thread-last
cons
(cons x seq)
conjと同じくSequenceに値を挿入する関数ですが、
こちらは主に先頭に挿入する関数になっています。
;; こちらはリストでもベクターでも同じ先頭への挿入になります
(cons 1 '(2 3 4 5 6))
;;=> (1 2 3 4 5 6)
;; 2個の引数にしか対応しておらず、複数要素を追加することはできません
(cons [1 2] [4 5 6])
;;=> ([1 2] 4 5 6)
drop
(drop n)
(drop n coll)
シーケンスから先頭の要素を捨てる関数。
実務ではあまり登場シーンは無いが、クイズではわりと登場機会がありそう。
(drop 2 [1 2 3 4])
;;=> (3 4)
filter
(filter pred)
(filter pred coll)
どの言語にも用意されている、シーケンスから該当する要素を抜き出す関数です。
(filter odd? (range 10))
;;=> (1 3 5 7 9)
;; ベクターで返して欲しい場合はfiltervもある(removevやkeepvはない)
(filterv even? (range 10))
;;=> [0 2 4 6 8]
group-by
(group-by f coll)
グループ毎に集計したい時に頻出する関数です。
他の言語やライブラリでもgroupByという関数が用意されており、特に違和感なく使えると思います。
;; ユーザIDでグループ分けする
(group-by :user-id [{:user-id 1 :uri "/"}
{:user-id 2 :uri "/foo"}
{:user-id 1 :uri "/account"}])
;;=> {1 [{:user-id 1, :uri "/"}
;; {:user-id 1, :uri "/account"}],
;; 2 [{:user-id 2, :uri "/foo"}]}
keep
(keep f)
(keep f coll)
結果がnil以外のものだけを残すmap
filter
とmap
を同時に行いたいケースで使うと無駄がありません。
(keep #(if (odd? %) %) (range 10))
;;=> (1 3 5 7 9)
(map #(if (odd? %) %) (range 10))
;;=> (nil 1 nil 3 nil 5 nil 7 nil 9)
map
(map f)
(map f coll)
(map f c1 c2)
(map f c1 c2 c3)
(map f c1 c2 c3 & colls)
clojureに限らず多くの言語に移植されているリスト操作の関数です。
地図ではなく、数学用語の写像に該当します。
;; 戻り値はベクターではなく遅延シーケンス(リスト)になる
(map inc [1 2 3 4 5])
;;=> (2 3 4 5 6)
;; ベクターが欲しければmapvを利用する
(mapv inc [1 2 3 4 5])
;;=> [2 3 4 5 6]
map-indexed
(map-indexed f)
(map-indexed f coll)
第一引数はインデックス値、第二引数が各々の要素になるmapです。
;; 一般的な例
(map-indexed #(when (< % 2) str % %2) [:a :b :c])
;;=> (:a :b nil)
;; for等でインデックス値が欲しい時に使うケースが多そう
(map-indexed vector [:a :b :c])
;;=> ([0 :a] [1 :b] [2 :c])
partition
(partition n coll)
(partition n step coll)
(partition n step pad coll)
n個のシーケンスをm個ずつのシーケンスに分割したいケースで使う少々ニッチな関数です。
他言語のライブラリではchunkという名前だったので、探す時に今だに苦戦します。
(partition 4 (range 20))
;;=> ((0 1 2 3) (4 5 6 7) (8 9 10 11) (12 13 14 15) (16 17 18 19))
remove
filterの逆でtrueになるものを捨てる関数です。
filterを使うと#(not (fn %))
という風な無名関数だらけになるケースをさらっと書ける必須関数です。
私はrejectで覚えていたので関数名が分からず苦労しました。
(remove nil? [1 nil 2 nil 3 nil])
;;=> (1 2 3)
string/join
(join coll)
(join separator coll)
シーケンスを結合して文字列を生成する関数です。
個人的に使用頻度が大きいので抜粋しました。
(use '[clojure.string :as string])
(string/join ", " [1 2 3])
;;=> "1, 2, 3"
;; SQLやCSVを生成するのに役に立つ
(->> [{:name "taro"} {:name "jiro"}]
(map #(str "'" (:name %) "'"))
(string/join ","))
;;=> "'taro','jiro'"
take
(take n)
(take n coll)
シーケンスの先頭からn個の要素を抜き出す関数です。
遅延シーケンスに対応しているので、range関数から無限の要素から抜き出す事も可能です。
(take 3 [1 2 3 4 5 6])
;;=> (1 2 3)
;; (range)単体を実行すると無限ループになるので注意
(->> (range)
(take 10))
;;=> (0 1 2 3 4 5 6 7 8 9)
zipmap
(zipmap keys vals)
シーケンスからMapを生成する際に重宝します。
(zipmap [:a :b :c :d :e] [1 2 3 4 5])
;;=> {:a 1, :b 2, :c 3, :d 4, :e 5}
Map
Mapの操作はClojureでは少々癖が強い印象を受けました。
もしかして全部reduce-kv
で頑張る必要があるかと震えていましたが、
同僚のウィザードさん達に相談したらinto
を教えて貰い割と簡単に書ける事に気付きました。
またmedleyというライブラリも紹介してもらいました。
こちらのライブラリはfilter-kb
やmap-kv
が用意されており、良い感じに扱えるようです。
引数1個
keys
(keys map)
Mapからキーのシーケンスを取り出す関数です。
(keys {:keys :and, :some :values})
;;=> (:keys :some)
;;MapのベクターからCSVの1行目を生成
(use '[clojure.string :as string])
(->> [{:name "taro" :age 20} {:name "jiro" :age 18}]
first
keys
(map name)
(string/join ","))
;;=> "name,age"
vals
(vals map)
Mapから値を取り出す関数です。
Mapは順番が保証されない(?)ので、使い所は少々限られます。
他言語のハッシュマップとして使うケースでは重宝するかもしれません。
(vals {:a "foo", :b "bar"})
;;=> ("foo" "bar")
->> thread-last
filter
(filter pred)
(filter pred coll)
Mapを起点にした場合、出力結果はList->Vector固定でMapでは無いことに注意してください。
into
関数を併用することでMap->Mapを実現出来ますので、filterもぐっと使い勝手がよくなります。
;; 値が2と3のものを抜き出す
(filter (comp #{2 3} last) {:x 1 :y 2 :z 3})
;;=> ([:y 2] [:z 3])
;; into関数を利用してMap->Mapを実現する
(into {} (filter (comp #{2 3} last)) {:x 1 :y 2 :z 3})
;;=> {:y 2, :z 3}
into
(into)
(into to)
(into to from)
(into to xform from)
2次元配列的なSequenceを作ってMapに流し込むと新しいMapが生成出来ます。
Mapを一度map
やfilter
で二次元配列的なSequenceに変換し、再度Mapに固め直すというアプローチになります。
挙動に癖はあるものの、シンプルに書ける事が多いように思えます。
;; 2次元配列的なベクターをMapに変換できます
(into {} [[:a "a"] [:b "b"]])
;;=> {:a "a", :b "b"}
;; 【注意】中身がリストだとエラーになります
(into {} ['(:a "a") '(:b "b")])
;;=> ClassCastException clojure.lang.Keyword cannot be cast to java.util.Map$Entry clojure.lang.ATransientMap.conj (ATransientMap.java:44)
;; filter的な事がしたい場合
(into {} (filter (comp #{2} val)) {:a 1 :b 2 :c 3})
;;=> {:b 2}
;; map的な事がしたい場合
(into {} (map #(update % 1 * 2)) {:a 1 :b 2 :c 3})
;;=> {:a 2, :b 4, :c 6}
map
(map-indexed f)
(map-indexed f coll)
Mapに対しても利用可能ですが、戻り値がListになるので注意してください。
Map->Mapで変換したい時はmap
関数とinto
関数を併用してください。
第一引数が[Key, Value]
のSequenceになるので、他の言語やライブラリのtoPairs的な事がしやすいのが特徴です。
;; [Key, Value]のSequenceに変換する
(map identity {:a 1 :b 2 :c 3})
;; => ([:a 1] [:b 2] [:c 3])
;; 中身が配列の場合
(map #(update % 1 (fn [it] (+ it 2))) {:a 1 :b 2 :c 3})
;;=> ([:a 3] [:b 4] [:c 5])
;; 戻り値をMapで受け取りたい
(into {} (map #(update % 1 (fn [it] (+ it 2))) {:a 1 :b 2 :c 3}))
(into {} (map #(update % 1 + 2)) {:a 1 :b 2 :c 3})
;;=> {:a 3, :b 4, :c 5}
reduce-kv
(reduce-kv f init coll)
reduceでもMapを扱えますが、第二引数が[Key, Value]
のSequenceになるので、
reduce-kvの方が扱いやすいでしょう。
実践的にはintoの方が簡素に書けるケースも多いので、両方のコードを書いてみて簡素な方を採用しましょう。
;; 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}
最後に
この数ヶ月間、業務で触って使ったものを中心に紹介していきました。
沢山ありそうでしたが、こうやってまとめると以外と見つからないものですね。
もしこの関数便利だよというものがありましたら教えてください。
また、Clojurianにとって遅延シーケンスって大事なんですかね?
完璧にこの辺すっ飛ばしてしまったので、もし重要でまとめ直した方がいいよと感じる方がいらしたら教えてください。