Clojure/FP:将每个参数的函数应用于运算符



假设我有几个向量

(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 中做到这一点,我可以使用一些标准库函数?
  • 这种类型的表达式有名字吗?

对于您的加法示例,我经常使用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 中非常常见,读者可以看到它如何适应现有模式。此外,如何为简单的映射和应用不起作用的用例创建新的归约函数应该是相当透明的。转导函数也可以与其他变换(如filtermapcat组成,用于具有更复杂的初始数据结构的情况。

您可能正在寻找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

我认为你不需要宏,你只需要参数化你的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参数是任意函数,但似乎您的用例主要涉及挖掘出要比较的元素,然后将任意函数应用于这些值。

我不确定这种方法是否比上面提供的传感器版本更好(当然效率不高(,但我认为当不需要这种效率时,它提供了一种更简单的替代方案。

我将这个过程分为三个阶段:

  1. 将集合中的项目转换为要操作的集合中的数据 上 -(map :name coll);
  2. 对集合中的转换项进行操作,返回结果集合 -(map = transf-coll-a transf-coll-b transf-coll-c)
  3. 最后,选择返回结果集合的结果 -(first calculated-coll)

在玩收藏时,我尝试将多个项目放入收藏中:

(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 中的第二个字符匹配项目,并返回排在第二位的项目的结果:

(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 

在换能器世界中,您可以使用序列函数,该功能也适用于多个集合:

(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)

最新更新