【问题标题】:How to observe progress while consuming a lazy sequence?如何在使用惰性序列时观察进度?
【发布时间】:2014-08-27 03:31:11
【问题描述】:

我想知道以下在使用序列时“观察”序列的方式是否正确。我已经阅读了以下 SO 答案,但我有点惊讶,因为我已经多次阅读“正确”的方法是使用惰性(因此即使它也明确地使你的序列变得惰性)不是),但是那里甚至没有提到 “懒惰”这个词

How to implement the Observer Design Pattern in a pure functional way?

所以基本上我有一个惰性序列,我想保持这个惰性序列 100% 纯净,我不希望观察者将任何东西泄漏到那个惰性序列中。在我的示例中,我只是使用(range 100000),但任何惰性序列都可以。

然后我使用一些可变性(在 Clojure 中使用原子)做这样的事情(代码是可运行的,您可以在 REPL 中复制/粘贴):

(let [a (atom (range 100000))]
  (loop [i 0]
    (if (= 0 (mod i 10)) (println "i: " i)) ; the observer
    (if (= 100 i)
      (take 1 @a)
      (do (reset! a (rest @a)) (recur (inc i))))))

重点不是我在这里使用了可变原子,而是惰性序列的实现根本不知道它正在被观察。观察者显然可以更花哨:就像实际通知观察者而不是使用副作用来打印 i(再一次:打印 i 这里只是一个例子)。

在使用惰性序列时,这是一种“正确”的观察方式吗?

如果这不正确,您在 Clojure 中使用惰性序列时会如何观察?

或者,您将如何在 Haskell 中执行此操作?

【问题讨论】:

  • 鉴于 clojure 序列是不可变的,并且观察者模式旨在观察变化(序列不会发生这种情况),您究竟想观察什么?
  • @JoostDiepenmaat:正如我在问题中所写:我在消费序列时“走多远”。例如,假设我想对前 10 亿个素数求和,并显示一个进度条,说明我在计算过程中的进度(并更新进度条,例如,每 100 万个素数)。但是我不想修改我的惰性序列生成素数,因为“观察”的概念与素数的生成是正交的。我只是做了那个例子,但我需要不时做一些事情来“看看”发生了什么。我希望这是有道理的。
  • 惰性序列甚至不需要是有限的:即使对于无限序列,更新 UI 以了解正在发生的事情也是有意义的。假设您正在通过分析(无限)素数惰性序列来寻找具有一些非常特定属性的素数,直到找到符合您的条件的素数:更新一次可能很方便,每 x i> 已测试的素数,UI,通过显示:"Tried xxx primes so far".

标签: haskell clojure observer-pattern lazy-evaluation


【解决方案1】:

这就是我在 Haskell 中的做法。您要的是一种惰性数据结构,它偶尔会产生一些价值。这可以建模为带有跳过的流。

data Stream a
    = Done 
    | Skip    (Stream a)
    | Yield a (Stream a) 

Stream 可以Yield 一个值和流的其余部分Skip 并返回流的其余部分,或者它是Done,这意味着流已被完全消耗。

我们可以评估一个产生副作用的Stream,方法是简单地递归它,执行Yield中的副作用,直到我们到达Stream的末尾。

eval :: Monad m => Stream (m a) -> m ()
eval Done         = return ()
eval (Yield ma s) = ma >> eval s
eval (Skip     s) = eval s

我们可以从惰性序列(在本例中为列表)构建Stream,给定一个“决策”函数,该函数选择是为原始序列中的每个元素产生副作用还是跳过。

observe :: Monad m => (a -> Maybe (m b)) -> [a] -> m ()
observe f = eval . go
    where go []     = Done
          go (x:xs) = case f x of
              Nothing -> Skip     (go xs)
              Just mb -> Yield mb (go xs)

如果我们想观察其他类型的序列(包括列表),我们可以将observe 泛化为适用于任何Foldable 实例,这个非常简洁的定义:

observe :: (Foldable f, Monad m) => (a -> Maybe (m b)) -> f a -> m ()
observe f = eval . foldr (maybe Skip Yield . f) Done

最终产品,

f :: Int -> Maybe (IO ())
f x | x `rem` 10 == 0 = Just (print x)
    | otherwise       = Nothing

main = observe f [0..100]

这也适用于无限序列。

【讨论】:

    【解决方案2】:

    如果你只是想在消费过程中散布一个副作用,那么是的,在 Clojure 中做的明智的事情是包装另一个惰性序列。

    (defn lazy-report
      "Wrap s in a lazy sequence that will call f, presumably for side effects, 
       on every nth element of s during consumption."
      [s f n] 
      (let [g (cons (comp first (juxt identity f)) (repeat (dec n) identity))]
        (map #(% %2) (rest (cycle g)) s)))
    

    (println "sum:" (reduce + (lazy-report (range 1 1000) #(println "at:" %) 100)))
    ;=> at: 100
    ;   at: 200
    ; ...
    ;   at: 900
    ;   sum: 499500
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-05-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多