Clojure
AdventCalendar
ClojureScript
ClojureDay 18

Prismatic/plumbing について調べてみた

More than 3 years have passed since last update.

フリーで仕事をしている snufkon といいます。最近は株式会社テンクーにて、Clojureを書いています。

今日は以前から調べよう、調べようと思いつつずっとTODOリストに埋もれていた Plumbing について調べてみました。観測した限りでは日本語で役に立つ情報もなさそうだったので、さわり程度に軽くいきたいと思います。

Plumbing とは?

Prismatic という会社が作っている Clojure のライブラリです。core の部分は cljx で書かれており Version 0.3.0 から ClojureScript をサポートしています。Prismatic というと schema の方をよく見かけますが、 github につけられている★の数をみてみると

  • schema: 787
  • plumbing: 1038

※ 2014-12-13 時点

となっており plumbing > schema だったりします。ついでに定番のライブラリがどれくらいの★を獲得しているか調べてみると...

という結果でした。実プロジェクトで使われるライブラリ達と遜色ない程度の★を確保しており潜在能力?がありそうな気がします。(あくまで★の数だけですが...)

この潜在能力を持った Plumbing ですが内部的に2つの機能群に分かれているようです。

  • Graph
  • plumbing.core

今回は、紙面の都合により Graph は扱いません。単純なユーティリティである plumbing.core にどんな関数があるかだけ紹介します。

plumbing.core

現在(2014-12-13)、plumbing.core には43個の関数&マクロがあります。そのうちのいくつかについて調べてみました。

plumbing のバージョンは0.3.5を利用しています。

  • マップ関連
  • スレッディングマクロ関連
  • When 関連
  • その他

と分類してありますが、ライブラリ側では特に分類等はしていません。

マップ関連

map-from-keys
(map-from-keys f ks)

ks(keys) の各 k(key) に対して (f k) を値にしたマップを作成

(map-from-keys #(count %) ["a" "bb" "ccc"])
;; {"a" 1, "bb" 2, "ccc" 3}

map-from-vals
(map-from-vals f vs)

vs(values) の各 v(value) に対して (f v) をキーにしたマップを作成

(map-from-vals #(-> % first str keyword) ["aaa" "bbb" "ccc"])
;; {:a "aaa", :b "bbb", :c "ccc"}

;; キーが重複する場合は後を残す
(map-from-vals #(-> % first str keyword) ["aaa" "bbb" "ccc" "aaaa"])
;; {:a "aaaa", :b "bbb", :c "ccc"}

map-keys
(map-keys f m)

m(map)の 各 k(key) に対して (f k) でキーを置換したマップを作成

(map-keys #(-> % name first str keyword) {:aa 1 :bb 2 :cc 3})
;; {:a 1, :b 2, :c 3}

;; キーが重複する場合は後を残す
(map-keys #(-> % name first str keyword) {:aa 1 :bb 2 :cc 3 :aaa 4})
;; {:a 4, :b 2, :c 3}

map-vals
(map-vals f m)

m(map)の 各 v(value) に対して (f v) で値を置換したマップを作成

(map-vals inc {:aa 1 :bb 2 :cc 3})
;; {:aa 2, :bb 3, :cc 4}

for-map
(for-map seq-exprs key-expr val-expr)

for の構文で最後に2つ(key-expr, val-expr)引数を取りマップを作成

(for-map [i (range 3)
           j (range 3)
           :let [s (+ i j)]
           :when (< s 3)]
           [i j]
           (even? s))
;; {[0 0] true, [0 1] false, [0 2] true, [1 0] false, [1 1] true, [2 0] true}

スレッディングマクロ関連

?>
(?> arg do-it? & rest)

->(thread-firstマクロ)内に条件付きのフォームを追加

(let [add-c? false]
  (-> {:a 1}
      (merge {:b 2})
      (?> add-c? (assoc :c 3))))
;; {:b 2, :a 1}

;; add-c? を true に変更
(let [add-c? true]
  (-> {:a 1}
      (merge {:b 2})
      (?> add-c? (assoc :c 3))))
;; {:c 3, :b 2, :a 1}      

?>>
(?>> do-it? & args)

->>(thread-lastマクロ)内に条件付きフォームを追加

(let [inc-all? false]
  (->> (range 10)
       (filter even?)
       (?>> inc-all? (map inc))))
;; (0 2 4 6 8)       

;; inc-all? を true に変更
(let [inc-all? true]
  (->> (range 10)
       (filter even?)
       (?>> inc-all? (map inc))))
;; (1 3 5 7 9)       

<-
(<- & body)

->>(thread-lastマクロ)内で->(thread-firstマクロ)の動作に変更

(->> (range 5)
     (map inc)
     vec
     (<- (conj 6)))
;; [1 2 3 4 5 6]     

as->>
(as->> name & forms-and-expr)

->>(thread-lastマクロ)内でas->と同様の機能を提供

(->> [1 2 3 4 5]
      (map inc)
      (as->> x (do (println "x:" x) x))
      (map inc))
;; (3 4 5 6 7)

fn->
(fn-> & body)

`(fn [x] (-> x ~@body)) と等価

(def func (fn-> (assoc :b 2) 
                 (assoc :c 3)))
(func {:a 1})
;; {:c 3, :b 2, :a 1}

when 関連

assoc-when
(assoc-when m & kvs)

value が truthy の場合のみ assoc

(assoc-when {:a 1 :b 2} :c 3 :d nil)
;; {:a 1, :b 2, :c 3}

;; assoc の場合
(assoc {:a 1 :b 2} :c 3 :d nil)
;; {:d nil, :c 3, :a 1, :b 2}

conj-when
(conj-when coll x)
(conj-when coll x & xs)

truthy の値のみ conj

(conj-when [1 2 3] nil 4)
;; [1 2 3 4]

;; conj の場合
(conj [1 2 3] nil 4)
;; [1 2 3 nil 4]

cons-when
(cons-when x s)

truthy の場合のみ cons

(cons-when 4 '(1 2 3))
;; (4 1 2 3)
(cons-when nil '(1 2 3))
;; (1 2 3)

;; cons の場合
(cons 4 '(1 2 3))
;; (4 1 2 3)
(cons nil '(1 2 3))
;; (nil 1 2 3)

count-when
(count-when pred xs)

(pred x) が truthy の場合のみ count

(count-when even? [1 2 3 4 5])
;; 2

update-in-when
(update-in-when m key-seq f & args)

key-seq が存在しない場合に m(map) を変更せずに返す update-in

(update-in-when {:a 1 :b 2} [:b] inc)
;; {:a 1, :b 3}
(update-in-when {:a 1 :b 2} [:c] inc)
;; {:a 1, :b 2}

;; update-in の場合
(update-in {:a 1 :b 2} [:b] inc)
;; {:a 1, :b 3}
(update-in {:a 1 :b 2} [:c] inc)
;; java.lang.NullPointerException: null...

その他

safe-get
(safe-get m k)

k(key)が存在しない場合に例外を投げる get

(safe-get {:a 1 :b 2} :b)
;; 2
(safe-get {:a 1 :b 2} :c)
;; java.lang.RuntimeException: Key :c not found in [:a :b]

;; get の場合
(get {:a 1 :b 2} :b)
;; 2
(get {:a 1 :b 2} :c)
;; nil

swap-pair!
(swap-pair! a f)
(swap-pair! a f & args)

[old-val new-val] を返す swap!

(let [a (atom 0)]
  (swap-pair! a inc))
;; [0 1]

;; swap! の場合
(let [a (atom 0)]
  (swap! a inc))
;; 1

positions
(positions f s)

s(sequence)の要素 x に対して (f x) が truthy となる位置(インデックス)

(positions number? [1 "2" 3 \4])
;; (0 2)

singleton
(singleton xs)

xs が 1要素の場合、(first xs)を返す

(singleton [1])
;; 1
(singleton [1 2 3])
;; nil

dissoc-in
(dissoc-in m [k & ks])

get-in, assoc-in 等の dissoc 版

(dissoc-in {:a 1 :b {:c 2 :d 3} :e 4} 
             [:b :d])
;; {:a 1, :b {:c 2}, :e 4}

;; 空のマップは取り除かれる
(dissoc-in {:a 1 :b {:c 3} :d 4} 
             [:b :c])
;; {:a 1, :d 4}           

おわりに

今回は、Plumbing の一部をなす plumbing.core 内のユーティリティライブラリについて紹介しました。1つ1つの関数はシンプルで、普段 Clojure を書いている人は似ている関数をいくつか書いたことがあるのではないでしょうか?

plumbing.core はユーティリティを提供しているだけなので、実プロジェクトにもすんなりと導入できそうです。また、ClojureScript をサポートしているところも嬉しいところです。逆に今回紹介しなかった Graph の方は、プロジェクト導入に少しばかりコストががかりますが、有効に使えるようになると大きな効果が期待できそうです。興味がある方は、Prismaticのブログ記事が分かりやすかったので一読してみることをお勧めします。機会がありましたら、Graph についても記事を書いてみたいと思います。