【问题标题】:pattern for implementing undo/redo in clojure在 clojure 中实现撤消/重做的模式
【发布时间】:2012-04-08 11:42:37
【问题描述】:

一般来说,在 clojure 或 fp 中实现撤消/重做功能是否有既定模式?

在 OO 语言中,我会使用命令模式,但由于它是关于状态的,我想知道在 clojure 中这样做是否是惯用的。

有没有可以提供帮助的库?

【问题讨论】:

  • 前面的问题应该是你是否真的需要这种状态突变。
  • @Alex Taggart:当然你真的没有(但我认为那是你的观点;)我只使用不可变对象(在 Java 中)编写了撤消/重做。您可以通过仅保存(用户输入)并通过在所需时间重播输入来重新创建“状态”来编写撤消/重做。因此,当您想从“t5 到 t4”撤消时,您不会从 t5 到 t4“倒回”,而是将输入从 t0 重放到 t4(并且因为您是以“功能方式”进行操作,所以您是保证以正确的状态结束)。适用于很多情况并大大简化了撤消/重做恕我直言......

标签: design-patterns clojure functional-programming


【解决方案1】:

与许多设计模式一样,您可以将其实现为 clojure 中的函数。这在一定程度上取决于您在程序中如何表示状态(refs、atoms、agents),过程非常相似。

您只需向您的状态添加一个观察器函数代理/引用/原子,每次有更新时将状态添加到撤消列表。那么您的撤消功能只是在撤消列表中查找它。这具有将您添加到撤消列表的良好效果,也允许重做

我的第一印象是refs 可能是解决此问题的正确工具,因为您将能够以协调的方式恢复它们,当然,除非您可以将程序状态减少到一个单一的身份(在Clojure 这个词的含义)那么您将不需要协调更新,并且代理可以工作。

【讨论】:

  • 谢谢。这听起来是一个不错的解决方案。但是看看我是否做对了:我有 3 个代表我的状态的裁判。我给他们每个人打电话 add-watch 。当它们中的一些或全部在事务中发生更改时,观察者会拍摄所有它们的快照并将其放入堆栈中。撤消功能将打开一个新事务并恢复我所有引用中的最后一个快照状态。
  • 是的。我想需要确保三个观察者不会将相同的状态也添加到堆栈中三次。
  • 正确。我也可以将所有状态信息聚合到一个原子中。这将使过渡更容易。
【解决方案2】:

好的,我让它像 Arthur Ulfeldt 建议的那样工作:

(defn cmd-stack [state-ref]
  (let [stack (atom ['() '()])]
    (add-watch state-ref :cmdhistory
           (fn [key ref old new]
             (let [redo-stack '()
                   undo-stack (conj (second @stack) old)]
             (reset! stack [redo-stack undo-stack]))))
    stack))

(defn can-redo? [stack]
  (not (empty? (first @stack))))

(defn can-undo? [stack]
  (not (empty? (second @stack))))

(defn undo! [stack state-ref]
  (let [redo-stack (first @stack)
        undo-stack (second @stack)
        current-state @state-ref
        last-state (first undo-stack)]
    (assert (can-undo? stack) "cannot undo")
    (reset! state-ref last-state)
    (reset! stack [(conj redo-stack current-state) (drop 1 undo-stack)])))

(defn redo! [stack state-ref]
  (let [redo-stack (first @stack)
        undo-stack (second @stack)
        current-state @state-ref
        last-state (first redo-stack)]
    (assert (can-redo? stack) "cannot redo")
    (reset! state-ref last-state)
    (reset! stack [(drop 1 redo-stack) (conj undo-stack current-state)])))

但我仍然不太明白为什么。由于撤消!并重做!函数更新正在被监视的原子,观察者不应该对此做出反应,从而通过将未完成的值放回命令堆栈来弄乱命令堆栈吗?

【讨论】:

  • 撤消撤消的问题需要思考。你想让两个连续的撤消等价于撤消然后重做吗?
  • 这个答案可能更好地表示为对原始问题的编辑。
  • @Arthur:第一个问题:不,我当然不会期待这样的行为。我不是这个意思。我的意思是,我预计上面的代码是错误的(就像你描述的那样),但它实际上表现正确。至少根据单元测试;-)。您的第二条评论:我最初也是这么想的,但后来发现,这篇文章实际上是对我最初问题的回答,只是一个引发了另一个问题的答案。
  • 将您的更改视为从左(旧)到右(新)的一系列状态可能会有所帮助。然后将撤消视为向左移动,将重做视为向右移动。那么唯一的问题是如果你不在最右边的状态会发生什么,你做出改变。我建议,如果可以的话,将当前更改与右侧的下一个状态进行比较。如果它相同,只需沿一个状态移动。如果没有,则将新状态推送为最近的左侧状态,并销毁右侧的所有状态。
  • 附言。抱歉,如果您的代码已经这样做了,但对我来说 Clojure 仍然是一种只写语言:)
猜你喜欢
  • 2016-02-18
  • 1970-01-01
  • 2011-11-14
  • 2015-08-10
  • 2011-03-10
  • 2020-08-11
  • 2021-06-04
  • 2012-07-02
  • 2014-04-20
相关资源
最近更新 更多