【问题标题】:Keeping State in a Purely Functional Language以纯函数式语言保持状态
【发布时间】:2011-05-20 11:17:00
【问题描述】:

我正在尝试弄清楚如何执行以下操作,假设您正在开发直流电机的控制器,您希望让它以用户设置的特定速度旋转,

(def set-point (ref {:sp 90})) (while true (let [curr (read-speed)] (controller @set-point curr)))

既然设置点可以通过网络应用程序随时更改,我想不出不使用 ref 的方法,所以我的问题是函数式语言如何处理这种事情? (即使示例是 clojure 中的,但我对总体思路很感兴趣。)

【问题讨论】:

    标签: haskell functional-programming clojure


    【解决方案1】:

    这不会回答你的问题,但我想展示这些事情是如何在 Clojure 中完成的。它可能会帮助稍后阅读本文的人,这样他们就不会认为他们必须阅读 monad、响应式编程或其他“复杂”主题才能使用 Clojure。

    Clojure 不是一种函数式语言,在这种情况下,将纯函数暂时搁置一旁并建模the inherent state of the system with identities 可能是个好主意。

    在 Clojure 中,您可能会使用其中一种引用类型。有几个可供选择,知道使用哪一个可能很困难。好消息是它们都支持统一更新模型,因此稍后更改引用类型应该非常简单。

    我选择了atom,但根据您的要求,使用refagent 可能更合适。

    电机是程序中的一个标识。它是一些 事物 在不同时间具有不同值的“标签”,这些值彼此相关(即电机的速度)。我在原子上放了一个:validator,以确保速度永远不会低于零。

    (def motor (atom {:speed 0} :validator (comp not neg? :speed)))
    
    (defn add-speed [n]
      (swap! motor update-in [:speed] + n))
    
    (defn set-speed [n]
      (swap! motor update-in [:speed] (constantly n)))
    
    > (add-speed 10)
    > (add-speed -8)
    > (add-speed -4) ;; This will not change the state of motor
                     ;; since the speed would drop below zero and
                     ;; the validator does not allow that!
    > (:speed @motor)
    2
    > (set-speed 12)
    > (:speed @motor)
    12
    

    如果您想更改电机标识的语义,您至少有两种其他参考类型可供选择。

    • 如果您想异步更改电机的速度,您可以使用代理。然后您需要将swap! 更改为send。例如,如果调整电机速度的客户端与使用电机速度的客户端不同,这将很有用,因此“最终”改变速度是可以的。

    • 另一个选项是使用ref,如果电机需要与系统中的其他身份协调,这将是合适的。如果选择此引用类型,则将 swap! 更改为 alter。此外,所有状态更改都在带有dosync 的事务中运行,以确保事务中的所有身份都以原子方式更新。

    在 Clojure 中建模身份和状态不需要 Monad!

    【讨论】:

    • 如果您发现错误,请编辑此答案。我最不想做的就是传播有关此主题的错误信息。
    【解决方案2】:

    对于这个答案,我将“一种纯函数式语言”解释为“一种排除副作用的 ML 风格的语言”,我将依次解释为“Haskell”,我将其解释为意义“GHC”。这些都不是绝对正确的,但鉴于您将其与 Lisp 衍生产品进行对比,并且 GHC 相当突出,我猜这仍然是您问题的核心。

    与往常一样,Haskell 中的答案有点花招,其中对可变数据(或任何具有副作用的东西)的访问的结构方式是,类型系统保证它“看起来”是纯粹的内部,同时生成一个具有预期副作用的最终程序。通常与 monads 的业务是其中很大一部分,但细节并不重要,主要是分散了问题的注意力。实际上,这只是意味着您必须明确说明副作用可能发生的位置和顺序,并且不允许“作弊”。

    可变性原语通常由语言运行时提供,并通过在运行时提供的某些 monad 中产生值的函数访问(通常为IO,有时更专业)。首先,我们看一下您提供的Clojure 示例:它使用ref,在the documentation here 中有描述:

    虽然 Vars 通过线程隔离确保对可变存储位置的安全使用,但事务引用 (Refs) 通过软件事务内存 (STM) 系统确保对可变存储位置的安全共享使用。 Refs 在其生命周期内绑定到一个存储位置,并且只允许在事务中发生该位置的突变。

    有趣的是,整段都直接翻译成 GHC Haskell。我猜“Vars”等同于Haskell's MVar,而“Refs”几乎可以肯定等同于TVar as found in the stm package

    因此,要将示例转换为 Haskell,我们需要一个创建 TVar 的函数:

    setPoint :: STM (TVar Int)
    setPoint = newTVar 90
    

    ...我们可以在这样的代码中使用它:

    updateLoop :: IO ()
    updateLoop = do tvSetPoint <- atomically setPoint
                    sequence_ . repeat $ update tvSetPoint
      where update tv = do curSpeed <- readSpeed
                           curSet   <- atomically $ readTVar tv
                           controller curSet curSpeed
    

    在实际使用中,我的代码会比这更简洁,但我在这里留下了更冗长的内容,希望不那么神秘。

    我想有人可能会反对这段代码不是纯代码并且使用的是可变状态,但是……那又怎样?在某个时候,一个程序将要运行,我们希望它进行输入和输出。重要的是我们保留了纯代码的所有好处,即使在使用它来编写具有可变状态的代码时也是如此。例如,我使用repeat 函数实现了无限循环的副作用;但是repeat 仍然是纯净的并且行为可靠,我无法用它做任何事情来改变它。

    【讨论】:

      【解决方案3】:

      Functional Reactive Programming 是一种以功能方式解决明显尖叫可变性(如 GUI 或 Web 应用程序)的问题的技术。

      【讨论】:

      • 免责声明:FRP 很酷,但我认为它还没有真正“出现”。阅读起来很棒,我预计它会在几年内成为一件大事,但在短期内,寻找其他地方可能更有效,我不确定这是否是我指向新人的地方。跨度>
      • 我认为取决于新人的兴趣。 FRP 启发了我关于纯功能 建模 的想法——仅仅因为我能想象它工作的唯一方式是副作用/杂质,这并不意味着它必须有副作用。然而,FRP 特别是,实施起来是一个相当大的挑战。所以这取决于这个问题是实用的还是哲学的。
      【解决方案4】:

      您需要的模式称为 Monads。如果您真的想进入函数式编程,您应该尝试了解 monad 的用途以及它们可以做什么。作为起点,我建议this link

      作为对 monad 的简短非正式解释:

      Monads 可以看作是在程序中传递的数据 + 上下文。这就是解说中经常用到的“宇航服”。您将数据和上下文一起传递,并将任何操作插入到这个 Monad 中。一旦数据被插入到上下文中,通常没有办法取回数据,你可以反过来进行插入操作,以便它们处理与上下文结合的数据。这样一来,就好像你把数据拿出来了,但如果你仔细观察,你永远不会这样做。

      根据您的应用程序,上下文几乎可以是任何东西。一种数据结构,它结合了多个实体、异常、选项或现实世界(i/o-monads)。在上面链接的论文中,上下文将是算法的执行状态,因此这与您的想法非常相似。

      【讨论】:

      • 仅作记录,monad 只是用 FP 语言表示状态的几种方式之一。
      • @jalf,还有什么其他方法?
      • 而且,状态或 I/O 只是众多单子中的一小部分。
      【解决方案5】:

      在 Erlang 中,您可以使用进程来保存值。像这样的:

      holdVar(SomeVar) ->
        receive %% wait for message
          {From, get} ->             %% if you receive a get
            From ! {value, SomeVar}, %% respond with SomeVar
            holdVar(SomeVar);        %% recursively call holdVar
                                     %% to start listening again
      
          {From, {set, SomeNewVar}} -> %% if you receive a set
            From ! {ok},               %% respond with ok
            holdVar(SomeNewVar);       %% recursively call holdVar with
                                       %% the SomeNewVar that you received 
                                       %% in the message
        end.
      

      【讨论】:

        猜你喜欢
        • 2020-05-12
        • 1970-01-01
        • 1970-01-01
        • 2010-10-30
        • 2020-12-07
        • 2013-03-27
        • 1970-01-01
        • 2021-07-01
        • 2020-06-13
        相关资源
        最近更新 更多