如何在Clojure中使reduce函数更易读?

4
一个reduce调用首先使用其f参数。从视觉上看,这通常是表单中最大的部分。 例如:
(reduce
 (fn [[longest current] x]
   (let [tail (last current)
         next-seq (if (or (not tail) (> x tail))
                    (conj current x)
                    [x])
         new-longest (if (> (count next-seq) (count longest))
                       next-seq
                       longest)]
     [new-longest next-seq]))
 [[][]]
 col))

问题在于,val参数(在本例中为[[][]])和col参数出现在下方,需要你的眼睛长途跋涉才能将其与f的参数匹配。

如果按照以下顺序排列,会更易读:

(reduceb val col
  (fn [x y]
    ...))

我应该实现这个宏,还是一开始就完全错了?

“迭代”可以比“归约”更易读... - Chris Murphy
reduce 能够解决的一小部分问题也可以通过 iterate 解决。但是同样地,mapfilter 和其他无数个函数根据你要解决的问题而言,可能会比 reduce 更易读。我肯定不会将 iterate 提名为一个突出的替代者,并且它对于这个问题没有任何帮助。 - amalloy
1
简单的解决方案就是给你传递到 reduce 函数中的函数命名,然后通过名称进行传递。 - Paul Gowder
Paul的评论可能会成为一个被接受的答案。此外,如果您保持相对一致的格式约定,使用lambda参数的reduce函数将随着使用而变得更易读。 - ctpenrose
我对你的代码感到困惑,因为 tail(last current)。通常期望看到 tailnextrest 的结果。 - Chris Murphy
6个回答

5

你不应该写那个宏,因为它可以很容易地改写成一个函数。虽然我也不是非常热衷于将其写成函数;如果你真的想把reduce和它的最后两个参数配对,你可以这样写:

(-> (fn [x y]
      ...)
    (reduce init coll))

就我个人而言,当我需要像这样的一个大函数时,我发现逗号实际上是一个很好的视觉锚点,使得更容易看出这两个表单在最后一行:

(reduce (fn [x y]
          ...)
        init, coll)

更好的方法通常是不要在一开始就写这么大的代码。在此,您将至少两个步骤合并成一个相当大而困难的步骤,尝试一次性找到最长的下降子序列。相反,尝试将集合拆分为下降子序列,然后取最大的那个。
(defn decreasing-subsequences [xs]
  (lazy-seq
    (cond (empty? xs) []
          (not (next xs)) (list xs)
          :else (let [[x & [y :as more]] xs
                      remainder (decreasing-subsequences more)]
                  (if (> y x) 
                    (cons [x] remainder)
                    (cons (cons x (first remainder)) (rest remainder)))))))

然后,您可以使用以下代码替换您的reduce
(apply max-key count (decreasing-subsequences xs))

现在,lazy函数并不比reduce函数更短,但是它只做了一件事情,这意味着它可以更容易地理解;此外,它有一个名称(给你一个提示,告诉你它应该做什么),并且它可以在需要基于递减子序列查找其他属性的上下文中重复使用,而不仅仅是最长的子序列。如果您将(> y x)中的>替换为函数参数,甚至可以更频繁地重用它,从而允许您基于任何谓词将其拆分成子序列。此外,正如前面提到的,它是懒惰的,因此您可以在任何reduce函数无法使用的情况下使用它。
说到易于理解,正如您所看到的,当我阅读它时,我误解了您的函数应该做什么。我将把将其转换为严格递增的子序列的任务留给您作为练习,因为在我看来,它看起来像是计算递减的子序列。

我想将valcolf的参数配对,而不是与单词reduce配对,使用->无法实现这一点。我试图解决的问题在我提供的示例中是次要的,我的可读性问题更为重要。也许我本可以选择一个更好的例子。但你的解决方案总是很有启发性 :)。 - Ken

1

您不必使用reduce或递归来获取降序(或升序)序列。在这里,我们按照从长到短的顺序返回所有降序序列:

(def in [3 2 1 0 -1 2 7 6 7 6 5 4 3 2])
(defn descending-sequences [xs]
  (->> xs
       (partition 2 1)
       (map (juxt (fn [[x y]] (> x y)) identity))
       (partition-by first)
       (filter ffirst)
       (map #(let [xs' (mapcat second %)]
               (take-nth 2 (cons (first xs') xs'))))
       (sort-by (comp - count))))

(descending-sequences in)
;;=> ((7 6 5 4 3 2) (3 2 1 0 -1) (7 6))

(partition 2 1) 可以得到所有可能的比较,partition-by 则可以标记连续下降的区间。此时您已经可以看到答案了,代码的其余部分是在删除不再需要的“负担”。

如果您想要升序序列,则只需将 < 改为 >

;;=> ((-1 2 7) (6 7))

如果像问题中所述,您只想要最长的序列,则将 first 放在线程最后宏的最后一个函数调用中。或者,将 sort-by 替换为:
(apply max-key count)

为了最大限度地提高可读性,您可以为操作命名:
(defn greatest-continuous [op xs]
  (let [op-pair? (fn [[x y]] (op x y))
        take-every-second #(take-nth 2 (cons (first %) %))
        make-canonical #(take-every-second (apply concat %))]
    (->> xs
         (partition 2 1)
         (partition-by op-pair?)
         (filter (comp op-pair? first))
         (map make-canonical)
         (apply max-key count))))

0

我理解你的痛苦...它们很难读懂。

我看到了两个可能的改进。最简单的方法是编写一个类似于Plumatic Plumbing defnk风格的包装器:

(fnk-reduce { :fn    (fn [state val] ... <new state value>)
              :init  []
              :coll  some-collection } )

因此,函数调用有一个单一的映射参数,其中每个3个部分都带有标签并且可以以任何顺序出现在映射文本中。

另一种可能性是仅提取减少表示式并为其命名。这可以是代码表达式内部或外部的名称:

(let [glommer (fn [state value] (into state value)) ]
   (reduce glommer #{} some-coll))

或者可能

(defn glommer [state value] (into state value)) 
(reduce glommer #{} some-coll))

一如既往,任何增加清晰度的方法都是首选。如果你还没有注意到,我非常喜欢Martin Fowler提出的引入解释性变量重构方法。 :)


我经常使用你的第二个解决方案,但它并不能使valcol参数更接近f的参数。你的第一个解决方案很好,满足了我的要求,但有点啰嗦,并且可能无法与线程宏很好地配合使用。我可能最终会选择通过函数重新组织参数。 - Ken

0

提前为发布一篇更详细的解决方案而道歉,因为您想要更简洁/清晰。

我们正处于Clojure transducers 的新时代,似乎您的解决方案是通过传递“最长”和“当前”的状态来进行记录。与其将该状态向前传递,不如使用有状态的转换器。

(def longest-decreasing
   (fn [rf]
     (let [longest (volatile! [])
           current (volatile! [])
           tail (volatile! nil)]
       (fn
         ([] (rf))
         ([result] (transduce identity rf result))
         ([result x] (do (if (or (nil? @tail) (< x @tail))
                           (if (> (count (vswap! current conj (vreset! tail x)))
                                  (count @longest))
                             (vreset! longest @current))
                           (vreset! current [(vreset! tail x)]))
                         @longest)))))))

在你否定这种方法之前,请意识到它只是给出了正确的答案,而你可以用它做一些不同的事情:

(def coll [2 1 10 9 8 40])
(transduce  longest-decreasing conj  coll) ;; => [10 9 8]
(transduce  longest-decreasing +     coll) ;; => 27
(reductions (longest-decreasing conj) [] coll) ;; => ([] [2] [2 1] [2 1] [2 1] [10 9 8] [10 9 8])

再说一遍,我知道这可能看起来有点长,但是与其他传输器一起编写它的潜力可能是值得努力的(不确定我的空间1是否会破坏它??)


0

我相信迭代可以成为reduce的更易读的替代品。例如,这里是迭代器函数,iterate将使用它来解决这个问题:

(defn step-state-hof [op]
  (fn [{:keys [unprocessed current answer]}]
    (let [[x y & more] unprocessed]
      (let [next-current (if (op x y)
                          (conj current y)
                          [y])
            next-answer (if (> (count next-current) (count answer))
                         next-current
                         answer)]
        {:unprocessed (cons y more)
         :current     next-current
         :answer      next-answer}))))

current会不断增加直到长度超过answer,此时会创建一个新的answer。每当条件op不满足时,我们就重新开始构建一个新的current

iterate本身返回一个无限序列,因此需要在iteratee被调用了足够次数后停止:

(def in [3 2 1 0 -1 2 7 6 7 6 5 4 3 2])
(->> (iterate (step-state-hof >) {:unprocessed (rest in)
                                  :current     (vec (take 1 in))})
     (drop (- (count in) 2))
     first
     :answer)
;;=> [7 6 5 4 3 2]

通常,您会使用drop-whiletake-while来在获得答案后立即停止。我们可以在这里这样做,但是不需要短路,因为我们预先知道step-state-hof的内部函数需要调用(- (count in) 1)次。这比计数少一次,因为它每次处理两个元素。请注意,first强制进行最终调用。

0

我希望这个表单的顺序是:

  1. reduce
  2. valcol
  3. f

我已经能够确定这在技术上满足我的要求:

> (apply reduce
    (->>
     [0 [1 2 3 4]]
     (cons
      (fn [acc x]
        (+ acc x)))))
10

但这并不是易于阅读的。

这看起来简单得多:

> (defn reduce< [val col f]
    (reduce f val col))
nil

> (reduce< 0 [1 2 3 4]
    (fn [acc x]
      (+ acc x)))
10

(< 是“参数向左旋转”的简写形式)。使用 reduce<,我可以在我的目光到达 f 参数时看到被传递给 f 的内容,因此我可以专注于阅读 f 的实现(这可能会相当长)。另外,如果 f 很长,我就不再需要通过查看缩进来确定它们属于上面更远的 reduce 符号的 valcol 参数。我个人认为这比在调用 reduce 之前将 f 绑定到一个符号更易读,尤其是因为 fn 仍然可以接受名称以增加清晰度。

这是一种通用解决方案,但这里的其他答案提供了许多解决我所举的示例问题的好方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接