Clojure関数型プログラミングまとめ
いわゆる関数型プログラミングのパラダイムで登場する用語のClojure実現方法.
Clojure: Function
defn で定義.
ref: 🔗Clojure - Learn Clojure - Functions 🔖Function
Clojure: 多重定義関数(Multi-arity Functions)
Multi-arity functionsをサポート. arityはアリティと発音する, 関数の取りうる個数. オーバーロードと同義.
(defn messenger
([] (messenger "Hello world!"))
([msg] (println msg)))
Clojure: 可変長引数関数(Variadic Arity Functions)
可変長引数(オプション引数)をサポートする関数, variadic = 可変長.
Clojureでは & を用いて指定する.
(defn hello [greeting & who]
(println greeting who))
オプション引数が関数呼び出してで指定されない場合はnilがbindされる.
注意点は, variadic valueはリストで束縛されること. これはClojure: apply で無名関数を可変長引数に適応したいという理由でこうなっていると推測している.
リストの中身を変数にbindして使うにはClojureの分配束縛が便利.
Vectorの場合.
(defn fn-test1 [a & b] b)
(fn-test1 1 2) ;=> (2)
&の後ろがlistになっていると, 基本的には(first b)みたいな取り出し方をしないといけないが, 分配束縛をつかうことで, それを省略して書くことができる.
(defn fn-test2 [a & [b]] b)
(fn-test2 1 2) ;=> 2
Mapの場合.
(defn fn-test3 [a & b] b)
(fn-test1 1 {:c 1}) ;=> ({:c 1})
(defn fn-test4 [a & {:as b}] b)
(fn-test3 1 {:c 1}) ;=> {:c 1}
ref. Clojure - Destructuring in Clojure
Variadic Arity はMulti-arityよりもパフォーマンスが落ちる
便利な反面インタプリタに負荷を与える. fix arityのほうがいい.
ref. Clojure Don’ts: Optional Arguments with Varargs – Digital Digressions by Stuart Sierra
何でもいいので引数を渡す
& _ で表現する.
(defn [& _]... )
Clojure: キーワード引数
キーワード引数はClojureにはない. しかしClojure: 分配束縛により実現ができる.
こう書く.
(defn hoge [& {:keys [a b]}]
... )
(hoge :a 1 :b 2)
これはこれで見やすいかもしれない.
(defn test [url & {:keys [a b c]}]
(println url a b c))
(test "https://google.com"
:a "foo"
:b "bar"
:c "zoo")
さらに, Clojure 1.11: キーワード引数をとる関数にマップが渡せるようになった.
ref. Clojure Language Update 2021 - Qiita
つまり両方いける.
(hoge :a 1 :b 2)
(hoge {:a 1 :b 2})
関数はSeqかAssociativeを入力しSeqかAssociativeを出力するというClojureのデータと関数の世界観に従ってコードを書くならば, あるfunctionにはいってきたMapをそのままサブの小さな関数に処理させたいときにthisではいってきたらそれになんかassocしてサブ関数にわたしたい. こういうケースにおいてMapをそのまま渡せちゃったほうが便利.
defun: Clojureでのパターンマッチマクロ
Clojure: 無名関数
Clojureでは無名関数は fn で定義する. リーダマクロ #() でも表現可能.
Clojure: constantly
constantlyは 引数を受け取って無名関数を返す.
APIなど3rd party のライブラリを使おうと思ったとき引数に関数を指定しないと使えないときに引数を渡すためのテクニック.
例外での注意
ハマって2時間の時間を溶かしたメモ. constantlyは定数を引数に渡すべきで関数をわたしてはいけない.
(defn hoge [req-fn]
(try
(req-fn)
(catch Throwable e
(println "help"))))
(hoge (constantly (fn [] "hi")))
このとき, hogeの引数としてconstantlyをつかうとhogeの手前で評価されるため, tryに入らない.
無名関数を渡すと評価はtryのなかで実施される.
(hoge #(fn [] "hi"))
Clojure: complement
関数を受取り, 関数を評価した結果の反対の真偽を返す無名関数を返す.
notのわかりやすい名前をつけるときに使える. (つまり defによって良い名前で束縛されることを期待).
(def not-empty? (complement empty?))
(not-empty? []) ;;=> false
(not-empty? [1 2]) ;;=> true
🤔complement不要論
complementとnotは同じ結果なのでわかりにくいだけ, 不要とのこと.
この話題に対する深堀りの議論.
Is complement and not effectively the same in Clojure? - Stack Overflow
たしかにnot と complementは同じ結果になり, それは実装をみるとさらにそれがわかる.
意味合いが違う, notは値を返してcomplementは関数値を返す, しかしまあ結果は同じ. あとは使うかどうかはその人の価値観かな.
わたしはcomplementは使ってもいいかな, 関数値を意識してコーディングしたいので.
tips: 任意引数を渡す
(fn [& _])で任意引数を受け取る無名関数が定義できる.
Clojure: 関数適用
- apply: 関数適用
- partial: 部分適用
- comp: 関数合成
Clojure: apply - 関数適用
無名関数fnを引数リストargsに適用する.
以下の2つが同じことをしている.
- (apply str [“str1” “str2” “str3”])
- (str “str1” “str2” “str3”)
引数リストというところがミソ. applyは & をつかったClojure: 可変長引数関数での活用を想定している.
(defn hoge [f & args]
(apply f args))
(hoge #(println %) [1 2 3])
これにおいて, argsは ([1 2 3])となり[1 2 3]ではない, へんなカッコがついている. なのでヘンナカッコを取り除いて関数を適用するためには applyが必要になる.
Clojure: partial - 部分適用
Clojureではpartialの表記を利用して部分適用する.
複数の引数を取る関数の場合, partialで適用できるのははじめの引数のみであることに注意(left-apply).
部分適用は一部の引数を固定した無名関数を返すことにすぎないため, 2つ目移行の任意の引数を固定するには自分で無名関数を書くことが必要.
(defn foo [x y z]
(+ x y z))
;; partialだとxしか固定できない.
(def foo1 (partial foo 1))
(def foo2
(fn [x z] (foo x 2 z)))
(def foo3
(fn [x y] (foo x y 3)))
partial非推奨. わかりにくい.
Clojure: comp - 関数合成
複数の部分関数を組み合わせるのはcompをつかう.
Threading Macros は フォームを評価した結果であり compは評価するための関数をまとめたものである.
(def proc-comp (comp proc1 proc2 proc3))
(def proc-next (fn [x] (proc3 (proc2 (proc1 x))))
(proc-comp x)
(proc-nest x)
(-> x
(proc1)
(proc2)
(proc3))
comp覚えなくていいかも.
個人的にはpartialはよくつかうし嫌いではない, 人次第かと.
Clojure: juxt
複数の関数を受け取って新しく関数を返し, その関数は受け取った引数をそれぞれの関数に適用した結果をvectorで返す.
ex.) ((juxt a b c) x) => [(a x) (b x) (c x)]
ref: filter と remove のふたつの結果を簡単に受け取る方法 - Qiita
multimethodでの応用も便利. あるMapを受け取ってkeyやある処理を元に得られた結果によって異なる関数をdispatchしたいときは以下のように書ける.
;; Define the multimethod
(defmulti service-charge (juxt account-level :tag))
;; Handlers for resulting dispatch values
(defmethod service-charge [::acc/Basic ::acc/Checking] [_] 25)
(defmethod service-charge [::acc/Basic ::acc/Savings] [_] 10)
(defmethod service-charge [::acc/Premium ::acc/Account] [_] 0)
🔧partial and applyのコンビネーション
partialで作成した関数をどう使うか.
partialは無名関数を返すため, defを利用してシンボルに束縛する.
しかし作成したものをそのまま利用するときは((partial hoge x) y)のような二重でカッコが並ぶようになる. これが見た目的に気持ち悪い場合は, applyを利用する.
(apply (partial hoge x) y)
やっていることは変わらないので好き嫌いだと思う.
Clojure: 分配束縛(Destructuring)
Clojure: 遅延評価(Lazy Evaluation)
遅延評価とは, 値が必要になるまで式の評価を遅らせること.
Clojureの話題だとClojure: 遅延シーケンス(Laziness Sequence)が話題になることが多く, 遅延評価は影に隠れがち. しかしその遅延シーケンスを推す裏には,
値が必要になるまでその評価を遅らせる
というClojureの哲学がある(ref 💡そもそもなぜClojureは設計思想として遅延評価なのか?).
tags: 🔖遅延評価
Clojureでの遅延評価関数(delay/force)
delayマクロで計算を遅延させる.
forceマクロで評価を強制実効する. forceはリーダマクロである@で代替できる.
ref. Clojure: atom/delay/future/promiseの比較
🤔計算を関数で包んで関数値を渡す
delayはあまり使われておらず, よく見かけるのは処理を関数にして関数呼び出し.
関数とはparensで囲まれた計算に名前をつけて定数にbindしたものにすぎない. そして関数は値と一緒に呼び出さない限り(評価しない限り)計算をしない.
💡部分的に評価して残りを遅延させるのがpartial
Clojure: 再帰(loop/recur)
関数型プログラミングが難しいといわれる理由の一つ. 実は再帰以外はそんなに難しくないし慣れもあると思う.
Clojure: 高階関数
Clojure: シーケンスライブラリ(map/filter/reduce)
Clojure 関数プログラミングTopics
💡Clojureでは自動カリー化はないので自分で無名関数でなんとかする
Clojureでは自動カリー化(automatic currying)をサポートしておらず, 部分適用関数partialとか無名関数で手動カリー化する.
たとえば,
(defn sum
"Sum two numbers together"
[number1 number2]
(+ number1 number2))
みたいな関数にこうするとエラー.
(sum 1)
;; => clojure.lang.ArityException
;; => Wrong number of args (1) passed to: functional-concepts/sum
なので, これが自力の手動カリー?
(defn sum-curry [x] (partial sum x y))
💡Prefer anonymous functions over complement, comp and partial, as this results in simpler code most of the time.
The Clojure Style GuideのAnonymous Functions vs complement, comp and partialより.
簡単にいうと, complement, comp, partialよりも無名関数を使いましょうということ. なぜならば大体の場合において無名関数のほうがコードがわかりやすい.
詳細は以下.
When should I prefer comp and partial to anonymous functions? - Clojure Q&A
まあそうかもしれない, Clojure: complementとか忘れちゃうので.
日本語のスタイルガイドのカリー化には無名関数よりpartialが望ましい との記載は間違っていると思う.
というよりたぶん翻訳がかかったあとに2019の記事で逆転した可能性あり. またカリー化はpartialが好ましいという表現は💡カリー化と部分適用の違い問題に直面するのでけっこう危ういかも.
🔗References
👉Related
up: 📂Clojure Core
- tags.