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

时间:2018-06-26 03:14:14

标签: clojure functional-programming operators higher-order-functions

假设我有几个向量

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

但是让我想知道对于这种事情是否有更高层次的抽象。

我经常发现自己正在比较/否则将通过应用于多个元素的单个合成来计算事物,但是map语法对我来说似乎有点不足。

如果我要自煮某种类型的运算符,我会想要类似

的语法
(-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中是否有惯用的方法来执行此操作,我可以使用某些标准库函数?
  • 这种表达形式有名称吗?

4 个答案:

答案 0 :(得分:7)

对于您的添加示例,我经常使用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)组合在一起。

答案 1 :(得分:2)

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

答案 2 :(得分:1)

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

我不确定这种方法是否优于上面提供的换能器版本(肯定不那么有效),但是我认为当不需要这种效率时,它提供了一种更简单的选择。

答案 3 :(得分:0)

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

  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 

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

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