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

您可能正在寻找 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 中很常见,读者可以看到它如何适合现有模式。此外,对于简单映射和应用不起作用的用例,如何创建新的归约函数应该是相当透明的。转换函数也可以与其他转换组合,例如 filtermapcat,用于初始数据结构更复杂的情况。

我认为你不需要宏,你只需要参数化你的 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. 将collections中的items转化为collections中你要操作的数据 在 - (map :name coll);
  2. 在 collection 秒内对转换后的项目进行操作,returning collection 个结果 - (map = transf-coll-a transf-coll-b transf-coll-c)
  3. 最后,选择结果 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)