Clojure/FP:将函数应用于运算符的每个参数
Clojure/FP: apply functions to each argument to an operator
假设我有几个向量
(def coll-a [{:name "foo"} ...])
(def coll-b [{:name "foo"} ...])
(def coll-c [{:name "foo"} ...])
而且我想看看第一个元素的名称是否相等。
我可以
(= (:name (first coll-a)) (:name (first coll-b)) (:name (first coll-c)))
但是随着更多函数的组合,这很快就会变得很累而且过于冗长。 (也许我想比较第一个元素名称的最后一个字母?)
直接表达计算的本质似乎很直观
(apply = (map (comp :name first) [coll-a coll-b coll-c]))
但这让我想知道是否有针对此类事物的更高级别的抽象。
我经常发现自己比较/以其他方式操作要通过应用于多个元素的单个组合来计算的事物,但地图语法对我来说看起来有点不对劲。
如果我要在家酿造某种运算符,我会想要像
这样的语法
(-op- (= :name first) coll-a coll-b coll-c)
因为大部分计算都用(= :name first)
表示。
我想要一个抽象应用于运算符和应用于每个参数的函数。也就是说,求和应该和比较一样容易。
(def coll-a [{:name "foo" :age 43}])
(def coll-b [{:name "foo" :age 35}])
(def coll-c [{:name "foo" :age 28}])
(-op- (+ :age first) coll-a coll-b coll-c)
; => 106
(-op- (= :name first) coll-a coll-b coll-c)
; => true
类似
(defmacro -op-
[[op & to-comp] & args]
(let [args' (map (fn [a] `((comp ~@to-comp) ~a)) args)]
`(~op ~@args')))
- 在 clojure 中是否有惯用的方法来执行此操作,我可以使用一些标准库函数?
- 这种表达式有名称吗?
您可能正在寻找 every?
函数,但我会通过分解它并命名子元素来提高清晰度:
(let [colls [coll-a coll-b coll-c]
first-name (fn [coll] (:name (first coll)))
names (map first-name colls)
tgt-name (first-name coll-a)
all-names-equal (every? #(= tgt-name %) names)]
all-names-equal => true
我会避免使用 DSL,因为没有必要,而且它会让其他人更难阅读(因为他们不知道 DSL)。保持简单:
(let [colls [coll-a coll-b coll-c]
vals (map #(:age (first %)) colls)
result (apply + vals)]
result => 106
你的加法例子,我经常用transduce
:
(transduce
(map (comp :age first))
+
[coll-a coll-b coll-c])
您的相等用例比较棘手,但您可以创建一个自定义缩减函数来保持类似的模式。这是一个这样的函数:
(defn all? [f]
(let [prev (volatile! ::no-value)]
(fn
([] true)
([result] result)
([result item]
(if (or (= ::no-value @prev)
(f @prev item))
(do
(vreset! prev item)
true)
(reduced false))))))
然后用作
(transduce
(map (comp :name first))
(all? =)
[coll-a coll-b coll-c])
语义与您的 -op-
宏非常相似,同时更加地道的 Clojure 和更可扩展。其他 Clojure 开发人员将立即了解您对 transduce
的用法。他们可能不得不研究自定义归约函数,但此类函数在 Clojure 中很常见,读者可以看到它如何适合现有模式。此外,对于简单映射和应用不起作用的用例,如何创建新的归约函数应该是相当透明的。转换函数也可以与其他转换组合,例如 filter
和 mapcat
,用于初始数据结构更复杂的情况。
我认为你不需要宏,你只需要参数化你的 op
函数和 compare
函数。对我来说,你的 (apply = (map (comp :name first) [coll-a coll-b coll-c]))
版本非常接近。
这是一种可以使其更通用的方法:
(defn compare-in [op to-compare & args]
(apply op (map #(get-in % to-compare) args)))
(compare-in + [0 :age] coll-a coll-b coll-c)
(compare-in = [0 :name] coll-a coll-b coll-c)
;; compares last element of "foo"
(compare-in = [0 :name 2] coll-a coll-b coll-c)
我实际上不知道你可以在字符串上使用 get
,但在第三种情况下你可以看到我们比较每个 foo
的最后一个元素。
这种方法不允许 to-compare
参数是任意函数,但您的用例似乎主要处理挖掘您想要比较的元素,然后对这些元素应用任意函数值。
我不确定这种方法是否比上面提供的换能器版本更好(当然效率不高),但我认为当不需要这种效率时,它提供了一种更简单的替代方法。
我会将这个过程分为三个阶段:
- 将collections中的items转化为collections中你要操作的数据
在 -
(map :name coll)
;
- 在 collection 秒内对转换后的项目进行操作,returning collection 个结果 -
(map = transf-coll-a transf-coll-b transf-coll-c)
- 最后,选择结果 collection 到 return -
(first calculated-coll)
玩collection时,我尝试将多个项目放入collection:
(def coll-a [{:name "foo" :age 43} {:name "bar" :age 45}])
(def coll-b [{:name "foo" :age 35} {:name "bar" :age 37}])
(def coll-c [{:name "foo" :age 28} {:name "bra" :age 30}])
例如,按 :name 中的第二个字符匹配项目,return第二位项目的结果:
(let
[colls [coll-a coll-b coll-c]
transf-fn (comp #(nth % 1) :name)
op =
fetch second]
(fetch (apply map op (map #(map transf-fn %) colls))))
;; => false
在传感器世界中,您可以使用 sequence 函数,该函数也适用于多个 collections:
(let
[colls [coll-a coll-b coll-c]
transf-fn (comp (map :name) (map #(nth % 1)))
op =
fetch second]
(fetch (apply sequence (map op) (map #(sequence transf-fn %) colls))))
计算年龄总和(对于同一级别的所有项目):
(let
[colls [coll-a coll-b coll-c]
transf-fn (comp (map :age))
op +
fetch identity]
(fetch (apply sequence (map op) (map #(sequence transf-fn %) colls))))
;; => (106 112)
假设我有几个向量
(def coll-a [{:name "foo"} ...])
(def coll-b [{:name "foo"} ...])
(def coll-c [{:name "foo"} ...])
而且我想看看第一个元素的名称是否相等。
我可以
(= (:name (first coll-a)) (:name (first coll-b)) (:name (first coll-c)))
但是随着更多函数的组合,这很快就会变得很累而且过于冗长。 (也许我想比较第一个元素名称的最后一个字母?)
直接表达计算的本质似乎很直观
(apply = (map (comp :name first) [coll-a coll-b coll-c]))
但这让我想知道是否有针对此类事物的更高级别的抽象。
我经常发现自己比较/以其他方式操作要通过应用于多个元素的单个组合来计算的事物,但地图语法对我来说看起来有点不对劲。
如果我要在家酿造某种运算符,我会想要像
这样的语法(-op- (= :name first) coll-a coll-b coll-c)
因为大部分计算都用(= :name first)
表示。
我想要一个抽象应用于运算符和应用于每个参数的函数。也就是说,求和应该和比较一样容易。
(def coll-a [{:name "foo" :age 43}])
(def coll-b [{:name "foo" :age 35}])
(def coll-c [{:name "foo" :age 28}])
(-op- (+ :age first) coll-a coll-b coll-c)
; => 106
(-op- (= :name first) coll-a coll-b coll-c)
; => true
类似
(defmacro -op-
[[op & to-comp] & args]
(let [args' (map (fn [a] `((comp ~@to-comp) ~a)) args)]
`(~op ~@args')))
- 在 clojure 中是否有惯用的方法来执行此操作,我可以使用一些标准库函数?
- 这种表达式有名称吗?
您可能正在寻找 every?
函数,但我会通过分解它并命名子元素来提高清晰度:
(let [colls [coll-a coll-b coll-c]
first-name (fn [coll] (:name (first coll)))
names (map first-name colls)
tgt-name (first-name coll-a)
all-names-equal (every? #(= tgt-name %) names)]
all-names-equal => true
我会避免使用 DSL,因为没有必要,而且它会让其他人更难阅读(因为他们不知道 DSL)。保持简单:
(let [colls [coll-a coll-b coll-c]
vals (map #(:age (first %)) colls)
result (apply + vals)]
result => 106
你的加法例子,我经常用transduce
:
(transduce
(map (comp :age first))
+
[coll-a coll-b coll-c])
您的相等用例比较棘手,但您可以创建一个自定义缩减函数来保持类似的模式。这是一个这样的函数:
(defn all? [f]
(let [prev (volatile! ::no-value)]
(fn
([] true)
([result] result)
([result item]
(if (or (= ::no-value @prev)
(f @prev item))
(do
(vreset! prev item)
true)
(reduced false))))))
然后用作
(transduce
(map (comp :name first))
(all? =)
[coll-a coll-b coll-c])
语义与您的 -op-
宏非常相似,同时更加地道的 Clojure 和更可扩展。其他 Clojure 开发人员将立即了解您对 transduce
的用法。他们可能不得不研究自定义归约函数,但此类函数在 Clojure 中很常见,读者可以看到它如何适合现有模式。此外,对于简单映射和应用不起作用的用例,如何创建新的归约函数应该是相当透明的。转换函数也可以与其他转换组合,例如 filter
和 mapcat
,用于初始数据结构更复杂的情况。
我认为你不需要宏,你只需要参数化你的 op
函数和 compare
函数。对我来说,你的 (apply = (map (comp :name first) [coll-a coll-b coll-c]))
版本非常接近。
这是一种可以使其更通用的方法:
(defn compare-in [op to-compare & args]
(apply op (map #(get-in % to-compare) args)))
(compare-in + [0 :age] coll-a coll-b coll-c)
(compare-in = [0 :name] coll-a coll-b coll-c)
;; compares last element of "foo"
(compare-in = [0 :name 2] coll-a coll-b coll-c)
我实际上不知道你可以在字符串上使用 get
,但在第三种情况下你可以看到我们比较每个 foo
的最后一个元素。
这种方法不允许 to-compare
参数是任意函数,但您的用例似乎主要处理挖掘您想要比较的元素,然后对这些元素应用任意函数值。
我不确定这种方法是否比上面提供的换能器版本更好(当然效率不高),但我认为当不需要这种效率时,它提供了一种更简单的替代方法。
我会将这个过程分为三个阶段:
- 将collections中的items转化为collections中你要操作的数据
在 -
(map :name coll)
; - 在 collection 秒内对转换后的项目进行操作,returning collection 个结果 -
(map = transf-coll-a transf-coll-b transf-coll-c)
- 最后,选择结果 collection 到 return -
(first calculated-coll)
玩collection时,我尝试将多个项目放入collection:
(def coll-a [{:name "foo" :age 43} {:name "bar" :age 45}])
(def coll-b [{:name "foo" :age 35} {:name "bar" :age 37}])
(def coll-c [{:name "foo" :age 28} {:name "bra" :age 30}])
例如,按 :name 中的第二个字符匹配项目,return第二位项目的结果:
(let
[colls [coll-a coll-b coll-c]
transf-fn (comp #(nth % 1) :name)
op =
fetch second]
(fetch (apply map op (map #(map transf-fn %) colls))))
;; => false
在传感器世界中,您可以使用 sequence 函数,该函数也适用于多个 collections:
(let
[colls [coll-a coll-b coll-c]
transf-fn (comp (map :name) (map #(nth % 1)))
op =
fetch second]
(fetch (apply sequence (map op) (map #(sequence transf-fn %) colls))))
计算年龄总和(对于同一级别的所有项目):
(let
[colls [coll-a coll-b coll-c]
transf-fn (comp (map :age))
op +
fetch identity]
(fetch (apply sequence (map op) (map #(sequence transf-fn %) colls))))
;; => (106 112)