模仿Python的yield的惯用Clojure方法

7

我正在遍历一个列表,在遍历过程中构建状态,偶尔当遇到特定的标记时,我会返回结果。如果是在Python中进行这个操作,我会利用yield惰性地生成结果,并在函数的局部范围中跟踪状态:

# this is simplified for illustration
def yielder(input_list):
    state = 0
    for item in input_list:
        if item = 'SENTINEL':
            yield state * 2
            state = 0
        else:
            state += item

yielder([1, 5, 2, 5, 'SENTINEL', 4, 6, 7]) # [26, 34]

我的第一种实现使用了reduce,但它不如yield好,因为:

  • 我在迭代之间传递的值既有循环状态,又有我想要生成的项,这似乎很笨拙。
  • 它不是惰性的。

iterate可以用来缓解后者,但我实际上并不想为每个输入项返回某些东西,所以它需要更多的操作。

在Clojure中做这件事的惯用方式是什么?


我认为lazy-seq可以给我想要的结果,如果我遇到标记时返回(cons yield-val (yielder ...)),否则只需返回(yielder ...) - aaronstacy
4个回答

7

您可以像您提到的那样使用lazy-seq自己构建它,或者您可以使用partitionreduce将问题分成阶段,然后将它们串在一起。我将使用thread-last宏来单独显示每个步骤:

user> (->> [1, 5, 2, 5, :SENTINEL, 4, 6, 7] ;; start with data
           (partition-by #(= :SENTINEL %))  ;; ((1 5 2 5) (:SENTINEL) (4 6 7))
           (take-nth 2)                     ;; ((1 5 2 5) (4 6 7))
           (map #(* 2 (reduce + %))))       ;; the map here keeps it lazy
(26 34)

然后直接使用lazy-seq:

user>  (defn x [items]
         (when (seq items)
           (lazy-seq (cons (* 2 (reduce + (take-while #(not= :SENTINEL %) items)))
                           (x (rest (drop-while #(not= :SENTINEL %) items)))))))
#'user/x
user> (x [1, 5, 2, 5, :SENTINEL, 4, 6, 7])
(26 34)

for 不起作用是因为循环在遇到 :while 条件时终止。 - aaronstacy
1
哦,我误解了,我会写一个不同的例子。 - Arthur Ulfeldt
这个回答了问题,谢谢!虽然它在语义上略有不同,因为原始算法是将项目相加并乘以二。 - aaronstacy
6
#(= :SENTINEL %) 更好地表达为 #{:SENTINEL} - amalloy

3

Tupelo库有一种使用lazy-gen/yield的方法,它模仿了Python生成器函数:

(ns xyz
  (:require [tupelo.core :as t] ))

(def data-1 [1 5 2 5 :SENTINEL 4 6 7] )
(def data-2 [1 5 2 5 :SENTINEL 4 6 7 :SENTINEL] )

(defn yielder [vals]
  (t/lazy-gen
    (let [state (atom 0)]
      (doseq [item vals]
        (if (= :SENTINEL item)
          (do
            (t/yield (* 2 @state))
            (reset! state 0))
          (swap! state + item))))))

(yielder data-1) => (26)
(yielder data-2) => (26 34)

请注意,原始问题描述存在错误,因为只有遇到:SENTENEL标记时才输出累积状态。对于data-1data-2的不同输出说明了这个问题。

0

虽然我更喜欢Arthur的第一种解决方案,但这也可以用低层次的风格来写,而不使用lazy-seq:

(defn f [xs]
  (loop [[x & xs :as xxs] xs, n 0, ret []]
    (cond (empty? xxs) (conj ret (* n 2))
          (= x :sentinel) (recur xs 0 (conj ret (* n 2)))
          :else (recur xs (+ n x) ret))))

1
lazy-seq 的目的是懒惰计算,但你在这里失去了它的优点。尝试使用 (interpose :SENTINEL (range)) 测试 Arthur 版本和你的版本,并使用 take 仅获取前几个元素。 - amalloy
哦,我明白了。我只考虑了非惰性输入。那么,我会采纳 Arthur 的建议 +1。 - athos

-1

这是我使用 reduce 的版本:

(def v [1 5 2 5 "SENTINEL" 4 6 7])

(defn r []
  (let [{:keys [result current]}
        (reduce (fn [acc x]
                  (case x
                    "SENTINEL" (-> acc
                                   (update-in [:result] conj (* 2 (:current acc)))
                                   (update-in [:current] (constantly 0)))
                    (update-in acc [:current] #(+ x %))))
                {:result [] :current 0} v)]
    (conj result (* 2 current))))

user> (r)
[26 34]

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