3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ClojureDocs逆引き

Last updated at Posted at 2019-11-19

https://clojuredocs.org/ を逆引きしたい。

いま直面している問題を解決するために「どんな関数を使えば実現できるか / 効率が良さそうか」を見つけやすくしたい。

コレクションの先頭に追加したい

conjだと末尾に追加しちゃうので、そうではなく先頭に追加したい。

こうする

(cons "v2okimochi" [1 2 3])
;;=> ("v2okimochi" 1 2 3)

conjは追加要素を第2引数に取るが、 consは追加要素を第1引数に取るみたい。

コレクションの中身が存在することを確かめたい

中身が存在する (つまり空ではない)ことの判定をやりたい。

こうする

(seq [])
;;=> nil

(seq [1 2 3])
;;=> (1 2 3)

dev> (seq {})
;;=> nil

dev> (seq {:a 1 :b nil})
;;=> ([:a 1] [:b nil])

中身があればシーケンスに変換されて返ってくる (空ならnilが返ってくる)ので、これを条件分岐に突っ込んでnil punningにすればおk。

ここからは余談だけど、

コレクションが空かどうかをtrue / falseで確かめる (empty?)がある。空ならtrue。

ただ、 (empty?)の逆を確かめる場合は、 (not (empty?))よりも (seq)が推奨されている ( (doc empty?)にそういうイディオムの注意書きがあった)

> (doc empty?)
-------------------------
clojure.core/empty?
([coll])
  Returns true if coll has no items - same as (not (seq coll)).
  Please use the idiom (seq x) rather than (not (empty? x))
;;=> nil

なんと (empty?)自体も内部で (seq)を使っている。

> (source empty?)
(defn empty?
  "Returns true if coll has no items - same as (not (seq coll)).
  Please use the idiom (seq x) rather than (not (empty? x))"
  {:added "1.0"
   :static true}
  [coll] (not (seq coll)))
;;=> nil

同じく (empty?)の逆を確かめられる (not-empty)もあるが、こちらも内部で (seq)を使っている。

> (source not-empty)
(defn not-empty
  "If coll is empty, returns nil, else coll"
  {:added "1.0"
   :static true}
  [coll] (when (seq coll) coll))
;;=> nil

結局みんな (seq)を使うし (seq)で確かめられるからイディオムとして (seq)を使う、らしい。
関数名的には (not-empty)のわかりやすさが好きだけど。。。。。。。

ちな (empty?)の存在意義に疑問を感じている記事もあるみたい。

cf. https://tech.toyokumo.co.jp/entry/2019/12/09/001811

(seq)で何でもやるならnil punning的にもむしろbooleanを扱う (empty?)のほうが「?」になりそうだし「せやな」のおきもちがある。

https://clojuredocs.org/clojure.repl/doc
https://clojuredocs.org/clojure.repl/source
https://clojuredocs.org/clojure.core/empty_q
https://clojuredocs.org/clojure.core/not-empty

Vector []などのコレクションをSet #{}にしたい

  • Vector [1 2 3]をSet #{1 2 3}にしたい
  • List '(1 2 3)をSet #{1 2 3}にしたい

こうする

(set [1 2 3])
;;=> #{1 3 2}

(set '(1 2 3))
;;=> #{1 3 2}

多重Vector [[] []...]からそれぞれ最初の値だけ集めたい

[[:a 5] [:b nil] [c: "okimochi"]][:a :b :c]にしたい。

こうする

(into [] (map first [[:a 5] [:b nil] [:c "okimochi"]]))
;;=> [:a :b :c]

https://clojuredocs.org/clojure.core/into
https://clojuredocs.org/clojure.core/map
https://clojuredocs.org/clojure.core/first

多重Vector内のどのリストにも存在する値だけ集めたい

こういう記事一覧的なリストがあって、

[{:title "おきもち記事v2"}
 {:title "v2-article"}
 {:title "そもそもおきもちというのは古来から"}
 {:title "古き良き記事"}]

これを3つの条件でそれぞれ絞り込んだ結果リストが多重リストに入っていて、

[[{:title "おきもち記事v2"} {:title "そもそもおきもちというのは古来から"}] ; "おきもち"を含む
 [{:title "おきもち記事v2"} {:title "v2-article"}] ; "v2"を含む
 [{:title "おきもち記事v2"} {:title "古き良き記事"}]] ; "記事"を含む

ここで どの条件で絞り込んでもヒットした記事 (つまりAND条件でヒットした記事)を出したい。

※ AND条件固定だったり条件数が固定だったりするなら最初から (and (= ...))みたいにしてfilter関数でも使えば良いけど、AND, OR条件を指定されたり条件数が任意でリストに入ってたりする場合どうしようかなというアレ

(reduce)が使えた。

(into [] (reduce clojure.set/intersection
                 (map set
                      [[{:title "おきもち記事v2"} {:title "そもそもおきもちというのは古来から"}]
                       [{:title "おきもち記事v2"} {:title "v2-article"}]
                       [{:title "おきもち記事v2"} {:title "古き良き記事"}]])))

;;=> [{:title "おきもち記事v2"}]

;; thread-last-macroならこう
(->> [[{:title "おきもち記事v2"} {:title "そもそもおきもちというのは古来から"}]
      [{:title "おきもち記事v2"} {:title "v2-article"}]
      [{:title "おきもち記事v2"} {:title "古き良き記事"}]]
     (map set)
     (reduce clojure.set/intersection)
     (into []))

;;=> [{:title "おきもち記事v2"}]

絞り込み結果リストをそれぞれsetにした上で、 (reduce)によってリスト内を順番に巡って共通集合を取り続けている (畳み込み演算のように見える)。

(reduce f coll) (reduce f val coll)

f should be a function of 2 arguments. If val is not supplied,
returns the result of applying f to the first 2 items in coll, then
applying f to that result and the 3rd item,

(reduce)に空リストを送ると (clojure.set/intersection)が引数なしで実行されてエラーになるので、 (if)とかで頑張る必要がある。。。

https://clojuredocs.org/clojure.core/into
https://clojuredocs.org/clojure.core/reduce
https://clojuredocs.org/clojure.set/intersection
https://clojuredocs.org/clojure.core/map
https://clojuredocs.org/clojure.core/set

Mapのkeyだけ集めたい

{:a 5 :b nil :c "okimochi"}[:a :b :c]にしたい。

(map first)を使うこともできるが、Mapの場合は (keys)でいい感じにできる。

(into [] (keys {:a 5 :b nil :c "okimochi"}))
;;=> [:a :b :c]

https://clojuredocs.org/clojure.core/into
https://clojuredocs.org/clojure.core/keys

Mapのvalueだけ集めたい

{:a 5 :b nil :c "okimochi"}[5 nil "okimochi"]にしたい。

(map last)を使うこともできるが、Mapの場合は (vals)でいい感じにできる。

(into [] (vals {:a 5 :b nil :c "okimochi"}))
;;=> [5 nil "okimochi"]

https://clojuredocs.org/clojure.core/into
https://clojuredocs.org/clojure.core/vals

名前空間内にある全てのMapを自動的に集めてコレクションにしたい

v2okimochi.mind名前空間を1ファイルにしていて、辞書のように(def)だけを置いていて、

v2okimochi/mind.clj
(ns v2okimochi.mind)

(def kanashii
  {:name "かなしい"
   :description "(´;ω;`)ブワッ"})

(def kuyashii
  {:name "くやしい"
   :description "ぐぎぎ"})

それらを全てまとめてコレクションとして取得するような仕組みをつくっているときに、

v2okimochi/core.clj
(ns v2okimochi.core
  (:require
   [v2okimochi.mind :as mind]))

(def all-nodes
  [mind/kanashii
   mind/kuyashii])

新たに (def okodayo {:name "おこだよ" :description "プンプン"})をv2okimochi.mind名前空間へ追加したりするけど、そういう追加がある度にv2okimochi.core名前空間にも追加しなきゃいけないの何とかならんか?の話題。

v2okimochi.mind名前空間にはdefしかないのだから、defを全部集めてきてくれんか?

たとえばこうする

v2okimochi/core.clj
(ns v2okimochi.core)

(defn all-nodes
  []
  (-> 'v2okimochi.mind
      ns-publics
      vals
      (map var-get))

(ns-publics)に名前空間のシンボルを突っ込むことで その名前空間にインターンされたパブリックなVarオブジェクト (なの?)をMap形式で全部もってきて、

(vals)で値のほう (ここではVarオブジェクトが取れる)だけにして、

(map)で(var-get)を全てのVarオブジェクトに適用して中身の値のコレクションにする。

つまり実質的には [mind/kanashii mind/kuyashii]と同じ一覧を得られるし、v2okimochi.mindのほうだけ追加しても読み込み直せば自動で更新される。
( clojure.tools.namespace.repl/refreshしたらいけてるっぽい)

all-nodes、defでもいいかなと思ったらrepl起動失敗してしまうのでdefnにして呼ばれたときだけ読むのが良いっぽい。

https://clojuredocs.org/clojure.core/ns-publics
https://clojuredocs.org/clojure.core/vals
https://clojuredocs.org/clojure.core/map
https://clojuredocs.org/clojure.core/var-get

Mapから 値がnilでないものだけ選別したい

参考 => https://stackoverflow.com/questions/9339939/filter-nil-values-from-clojure-map

Mapが {:a 5 :b nil :c "okimochi"}のとき、
{:a 5 :c "okimochi"}にしたい ( :b nilにはご退場いただきたい)

こうする

(into {} (filter (comp some? val) {:a 5 :b nil :c "okimochi"}))
;;=> {:a 5, :c "okimochi"}

https://clojuredocs.org/clojure.core/into
https://clojuredocs.org/clojure.core/filter
https://clojuredocs.org/clojure.core/comp
https://clojuredocs.org/clojure.core/some_q
https://clojuredocs.org/clojure.core/val

(into {} ...{}(array-map)(hash-map)と書くこともできる。

1行が長いようならこう書き直すこともできる。

(->> {:a 5 :b nil :c "okimochi"}
     (filter (comp some? val))
     (into {}))
;;=> {:a 5, :c "okimochi"}

https://clojuredocs.org/clojure.core/array-map
https://clojuredocs.org/clojure.core/hash-map
https://clojuredocs.org/clojure.core/-%3E%3E ;; (->>)

(filter val)に変えて1行自体を短くすることもできる(こっちのほうが良いかも?)

(into {} (filter val {:a 5 :b nil :c "okimochi"}))
;;=> {:a 5, :c "okimochi"}

Mapの中身を取り出して関数の引数として使いたい

(the-function :name "v2okimochi" 
              :conditions [{:id 1}])

みたいに、引数の渡し方が なんかマップの中身をわざわざ取り出したような形式になる場合をなんとかしたい。

(the-function {:name "v2okimochi" :conditions [{...}]})みたいにマップを渡せれば良かったのに。独自にラッパー関数を用意して共通化しようとする時に超困る。

具体的にはこれをなんとかするために頑張っていた。

cf. https://github.com/mcohen01/amazonica#dynamodbv2

こうする

(apply println 
       (->> {:a 1 :b 2 :c nil :d 4}
            (filter val)
            (apply concat)))

;;=> :a 1 :b 2 :d 4

マップに対して (filter val)でnilのキーを弾き、
返ってきた ([:a 1] [:b 2] [:d 4])こんな感じのシーケンスを (apply concat)で1シーケンスとしてまとめ、
中身を順番に printlnの引数にする。
つまり最終的には (println :a 1 :b 2 :d 4)してるのと一緒。

apply力が強い。

cf. https://ayato-p.github.io/clojure-beginner/idioms/index.html#id3

https://clojuredocs.org/clojure.core/apply
https://clojuredocs.org/clojure.core/println
https://clojuredocs.org/clojure.core/-%3E%3E ;; (->>)
https://clojuredocs.org/clojure.core/filter
https://clojuredocs.org/clojure.core/val
https://clojuredocs.org/clojure.core/concat

Map内にあるMapの好きな要素を変えたい

{:are 1
 :sore {:v2 0
        :okimochi 999}
 :kore 3}

のとき、 :soreの中にある :okimochiの値を "みょーん"に変えたい。

こうする

(assoc-in {:are 1 
           :sore {:v2 0 
                  :okimochi 999} 
           :kore 3} 
          [:sore :okimochi] 
          "みょーん")
;;=> {:are 1, :sore {:v2 0, :okimochi "みょーん"}, :kore 3}

Mapのキー名を変えたい

{:name "v2okimochi"
 :mind ":innocent:"
 :version 2}

:name:namaeに変えたい。

こうする。

(clojure.set/rename-keys {:name "v2okimochi"
                          :mind ":innocent:"
                          :version 2}
                          {:name :namae})
;;=> {:mind ":innocent:", :version 2, :namae "v2okimochi"}

2番目の引数で 変えたい対象キー変更後のキー名を指定している。

複数のキー名を変えることもできる。

(clojure.set/rename-keys {:name "v2okimochi"
                          :mind ":innocent:"
                          :version 2}
                          {:name :namae
                           :mind :okimochi
                           :version :?????})
;;=> {:namae "v2okimochi", :okimochi ":innocent:", :????? 2}

ネストしているかもしれないMapの「文字数が3を超えている文字列」を全て短く切り取りたい

{:a "summarize target!"
 :b "123"
 :c 12345
 :d [1 2 3 4 5]
 :e {:a "123"
     :b "summarize target!"
     :c 12345
     :d [1 2 3 4 5]
     :e {:a 12345
         :b "123"
         :c "summarize target!"
         :d [1 2 3 4 5]}}}

こういう、

  • keyの数は数個レベルだけど、名前も個数もわからない
  • valueは文字列かもしれないしリストかもしれないしMapがネストしているかもしれない
  • Mapがネストする回数はせいぜい3回以内だけど、どこがどこまでネストしているかわからない

みたいなMapの中に、たまに5万文字くらいのすごい文字列のvalueが潜んでいるとして、
そういう文字は邪魔だから最初の3文字だけあればおk、それ以外は別に困ってないので手をつけない、ということをしたい。

こうする

(defn summarize-recursively
  [target-map]
  (->> (map (fn [[map-key map-value]]
              (let [str-max-length 3
                    summarized-value
                    (cond
                      (string? map-value) (let [length (count map-value)]
                                            (if (> length str-max-length)
                                              (subs map-value 0 str-max-length)
                                              map-value))
                      (map? map-value) (summarize-recursively map-value)
                      :else map-value)]
                {map-key summarized-value}))
            target-map)
       (apply merge)))

(summarize-recursively
 {:a "summarize target!"
  :b "123"
  :c 12345
  :d [1 2 3 4 5]
  :e {:a "123"
      :b "summarize target!"
      :c 12345
      :d [1 2 3 4 5]
      :e {:a 12345
          :b "123"
          :c "summarize target!"
          :d [1 2 3 4 5]}}})

;;=>
{:a "sum",
 :b "123",
 :c 12345,
 :d [1 2 3 4 5],
 :e
 {:a "123",
  :b "sum",
  :c 12345,
  :d [1 2 3 4 5],
  :e {:a 12345, :b "123", :c "sum", :d [1 2 3 4 5]}}}

https://clojuredocs.org/clojure.core/map
https://clojuredocs.org/clojure.core/cond
https://clojuredocs.org/clojure.core/count
https://clojuredocs.org/clojure.core/if
https://clojuredocs.org/clojure.core/%3E
https://clojuredocs.org/clojure.core/subs
https://clojuredocs.org/clojure.core/string_q
https://clojuredocs.org/clojure.core/map_q
https://clojuredocs.org/clojure.core/apply
https://clojuredocs.org/clojure.core/merge

ednファイルをMapとして読み込みたい

config.edn
{:name "v2okimochi"
 :settings {:status 500
            :ok? false}}

こういう設定ファイル config.ednが、
プロジェクトのリソースディレクトリ下 ( project.clj:resource-pathsに指定した場所)に置いてあるとして、

こうする

(-> "config.edn"
    io/resource
    slurp
    edn/read-string)
;;=> {:name "v2okimochi", :settings {:status 500, :ok? false}}

;; これをやるのと同じ
;; (edn/read-string (slurp (io/resource "config.edn")))

まず (io/resource)config.ednのリソース情報を取得し、
その中身を (slurp)でずるずるさせ (戻り値としては文字列が出てくる)、
それを (edn/read-string)で読み込み、Mapにする。

https://clojuredocs.org/clojure.core/-%3E ;; (->)
https://clojuredocs.org/clojure.java.io/resource
https://clojuredocs.org/clojure.core/slurp

また、このタイミングで 環境変数やファイルの読み込みも行いたい場合は、
たとえばこのように、

config.edn
{:v2okimochi-config
 {:my-env #v2/env "OKIMOCHI_ENV"
  :description "v2okimochi"}}

v2/envとか独自の タグ付きリテラルを仕込んだ上で、

(->> "config.edn"
     io/resource
     slurp
     (edn/read-string {:readers {'v2/env 'System/getenv}}))
;;=> {:v2okimochi-config {:my-env "value-of-env", :description "v2okimochi"}}

;; これをやるのと同じ
;; (edn/read-string {:readers {'v2/env 'System/getenv}}
;;                   (slurp (io/resource "config.edn")))

のように タグ付きリテラルと対応するdata reader function(edn/read-string)で指定すれば良さそう。

タグ付きリテラルがある my-envの値は、指定したリーダー関数 (System/getenv)に渡される。

なので、Mapとして返った際の :my-envの値は (System/getenv "OKIMOCHI_ENV")の結果 (環境変数が存在すればその値が、なければnil)となる。

※ 環境変数を取得する関数 (System/getenv)JVMが持っている機能のようで、ClojureDocsに載っていない

https://clojuredocs.org/clojure.core/-%3E%3E ;; (->>)


ちなみに以下は 非推奨 (関数の実行が危険だから?)

config.edn
{:v2okimochi-config
 {:my-env (System/getenv "OKIMOCHI_ENV")
  :description "v2okimochi"}}

みたいな 関数を含む設定ファイル config.ednがあるとして、それをこうする。

(->  "config.edn"
     (clojure.java.io/resource)
     (slurp)
     (read-string)
     (eval))

;;=> {:v2okimochi-config {:my-env "value-of-env", :description "v2okimochi"}}

;; これをやるのと同じ
;; (eval (read-string (slurp (clojure.java.io/resource "config.edn"))))

(clojure.java.io/resource)でファイルの中身を読み込み、
(slurp)でずるずるし (戻り値としては文字列を吐き出し)、
(read-string)でMapとして読み込み、
(eval)で設定ファイル内の関数を評価する。

設定ファイル内の :my-env (System/getenv "OKIMOCHI_ENV")は、 (read-string)しただけでは未評価の関数が入ったままとなる。 (eval)で評価することによって、 :my-env "value-of-env"のように評価後の値が入る。

↑これ、 (read-string)のdocstringを見ると 関数を実行できるから信頼できるソース以外では非推奨っぽいことが書かれている。
( (clojure.edn/read-string)を使え、とも書かれている)

(doc read-string)
-------------------------
clojure.core/read-string
([s] [opts s])
  Reads one object from the string s. Optionally include reader
  options, as specified in read.

  Note that read-string can execute code (controlled by *read-eval*),
  and as such should be used only with trusted sources.

  For data structure interop use clojure.edn/read-string
;;=> nil

https://clojuredocs.org/clojure.core/read-string
https://clojuredocs.org/clojure.core/eval

任意の数だけ :sushi: をデプロイしたい

for文のおきもちに馴染んだ民にとって直感的な書き方だとこう。

(loop [i 0]
        (when (< i 5) 
          (print ":sushi:") 
          (recur (+ i 1))))
:sushi::sushi::sushi::sushi::sushi:;;=> nil

:sushi::sushi: と表示されるので実質 :sushi: :sushi: :sushi: :sushi: :sushi:

https://clojuredocs.org/clojure.core/loop
https://clojuredocs.org/clojure.core/when
https://clojuredocs.org/clojure.core/print
https://clojuredocs.org/clojure.core/recur

一方で、Clojureのおきもちに沿うとこうなる。

(clojure.string/join (repeat 5 ":sushi:"))
;;=> ":sushi::sushi::sushi::sushi::sushi:"

これも実質 :sushi: :sushi: :sushi: :sushi: :sushi:

https://clojuredocs.org/clojure.string/join
https://clojuredocs.org/clojure.core/repeat

渡した関数をただ実行するようなマクロをつくりたい

(defmacro with-nothing
  "渡された関数 `func`をただ実行するマクロ"
  [func]
  `~func)
(macroexpand '(with-nothing (+ 1 2)))
;;=> (+ 1 2)

dev> (with-nothing (+ 1 2))
;;=> 3

需要がないと思うんですけど (名推理)

Placeholder

pedestalのまね

image.png

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?