如何使 Clojure 中的 reduce 更具可读性?

How to make reduce more readable in Clojure?

A 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]
    ...))

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

你当然不应该写那个宏,因为它很容易写成一个函数。不过,我也不太热衷于将它写成一个函数;如果你真的想将 reduce 与其最后两个参数配对,你可以这样写:

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

就个人而言,当我需要像这样的大型函数时,我发现逗号实际上是一个很好的视觉锚点,并且更容易分辨出最后一行有两种形式:

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

更好的是通常一开始就不要写这么大的 reduce。在这里,您将至少两个步骤合并为一个相当大且困难的步骤,方法是尝试一次找到所有最长的递减子序列。相反,尝试将集合拆分为递减的子序列,然后取最大的一个。

(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 的情况下使用它。

说到易懂性,如您所见,我在阅读时误解了您的函数应该做什么。我将把将其转换为严格递增子序列的任务留给您作为练习,在我看来您正在计算递减子序列。

我感受到你的痛苦......他们可能很难阅读。

我看到了 2 项可能的改进。最简单的就是写一个类似to the Plumatic Plumbingdefnk风格的wrapper:

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

所以函数调用有一个 map arg,其中 3 个部分中的每一个都被标记并且可以在 map 文字中以任何顺序出现。

另一种可能性是只提取还原 fn 并为其命名。这可以是包含 reduce:

的代码表达式的内部或外部
(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 的 Introduce Explaining Variable 重构思想的忠实拥护者。 :)

您不必使用 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 允许您标出连续减少的运行。此时您已经可以看到答案,其余代码正在删除不再需要的 baggage

如果你想要升序,那么你只需要将 < 更改为 >:

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

对于您想要更多 brevity/clarity.

发布 更长 解决方案,我会提前道歉

我们正处于 clojure transducers 的新时代,您的解决方案似乎通过了 "longest" 和 "current" 前向记录保存。有状态的传感器可以解决问题,而不是向前传递该状态。

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

同样,我知道这可能看起来更长,但与其他换能器组合的潜力可能值得付出努力(不确定我的 airity 1 是否打破了它??)

我相信 iterate 可以成为 reduce 更易读的替代品。例如这里是 iteratee 函数 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 本身 returns 一个无限序列,因此需要在 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 强制执行最终调用。

我想要这个表格的订单:

  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

(对于 "parameters are rotated left",< 是 shorthand)。使用 reduce<,当我看到 f 参数时,我可以看到传递给 f 的内容,因此我可以专注于阅读 f 实现(可能会很长)。此外,如果 f 确实变长了,我不再需要目视检查 valcol 参数的缩进来确定它们属于 reduce 符号的方式更远向上。我个人认为这比在调用 reduce 之前将 f 绑定到一个符号更具可读性,特别是因为 fn 仍然可以接受一个名称来清楚。

这是一个通用的解决方案,但此处的其他答案提供了许多很好的替代方法来解决我作为示例给出的特定问题。