【问题标题】:Clojure/FP: apply functions to each argument to an operatorClojure/FP:将函数应用于运算符的每个参数
【发布时间】:2018-06-26 03:14:13
【问题描述】:

假设我有几个向量

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

【问题讨论】:

  • 编写宏通常是惯用的方式,前提是使用标准函数不可能。如果你想要这样的语法,你几乎不得不使用宏,这基本上是在创建一个 DSL,这不一定是坏事。
  • 不,这里绝对不需要宏,除非你在真的不需要宏时一心想创建一个 DSL。

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


【解决方案1】:

对于你的加法示例,我经常使用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,用于具有更复杂的初始数据结构的情况。

【讨论】:

  • 我只是在准备类似的答案,但你打败了我(=, 与 transcduce 确实很棘手)。让我添加 fn -op-tr-,它替换 OP 的 -op- 宏:(defn -op-tr- [[op & to-comp] & args] (transduce (apply comp (map #(map %) (reverse to-comp))) op args))(-op-tr- [+ :age first] coll-a coll-b coll-c)(-op-tr- [(all? =) :name first] coll-a coll-b coll-c)
  • 酷!我不会想到在这里应用换能器。将论点视为一个序列对我来说似乎是令人愉快的 lisp-ey 并且它在正确的级别上进行了抽象。谢谢!
【解决方案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

【讨论】:

  • 感谢您的建议!我认为分解这些部分确实使其更具可读性,但我真的在寻找两者的抽象 i)每个 arg (comp :name first) 的转换 ii)运算符 =。该解决方案适用于相等性测试,但不会抽象出接受多个事物并返回单个事物的不同运算符。 (即= 的形状为[& args] => boolean)更新了问题以澄清。
【解决方案3】:

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

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

【讨论】:

  • 我认为您在描述我的用例时一针见血。我喜欢这个作为宏的更好替代品!
【解决方案4】:

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

  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)

【讨论】:

    猜你喜欢
    • 2011-08-06
    • 2018-12-19
    • 2015-02-04
    • 2017-10-08
    • 1970-01-01
    • 2016-10-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多