保持国家纯粹的功能语言

时间:2011-05-20 11:17:01

标签: haskell functional-programming clojure

我正在试图弄清楚如何执行以下操作,假设您正在使用直流电机的控制器,以保持其以用户设定的特定速度旋转,


(def set-point (ref {:sp 90}))

(while true
  (let [curr (read-speed)]
    (controller @set-point curr)))


既然设置点可以随时通过web改变应用程序,我想不出一种方法可以在不使用ref的情况下做到这一点,所以我的问题是函数式语言如何处理这类事情? (尽管这个例子是在clojure中我对一般的想法感兴趣。)

5 个答案:

答案 0 :(得分:15)

这不会回答你的问题,但我想说明这些事情是如何在Clojure中完成的。它可能有助于某人稍后阅读,因此他们认为他们不必阅读monad,反应性编程或其他“复杂”主题来使用Clojure。

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

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

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

电机是程序中的标识。它是某些 thing 的“标签”,它在不同的时间具有不同的值,并且这些值彼此相关(即,电机的速度)。我在原子上加了一个: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来模拟身份和状态!

答案 1 :(得分:8)

对于这个答案,我将把“纯粹的功能性语言”解释为“排除副作用的ML风格的语言”,我将依次将其解释为“Haskell”,我将其解释为含义“GHC”。这些都不是严格正确的,但鉴于你将它与Lisp衍生物形成鲜明对比并且GHC相当突出,我猜这仍然是你问题的核心。

与往常一样,Haskell中的答案有点狡猾,其中可变数据(或任何具有副作用的东西)的访问以这样的方式构造,即类型系统保证它“看起来”纯粹的内部,同时产生一个有预期副作用的最终程序。 monad的常见业务是其中很大一部分,但细节并不重要,大多分散注意力。在实践中,它只是意味着您必须明确可能发生的副作用和顺序,并且不允许您“作弊”。

Mutability原语通常由语言运行时提供,并通过在运行时也提供的某些monad中生成值的函数进行访问(通常为IO,有时更为专用)。首先,让我们看一下您提供的Clojure示例:它使用ref中描述的TVar

  

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

有趣的是,整个段落直接转换为GHC Haskell。我猜“Vars”相当于the documentation here,而“Refs”几乎肯定等同于Haskell's MVar

因此,要将示例转换为Haskell,我们需要一个创建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函数实现了无限循环的副作用;但{{1}}仍然是纯粹的,行为可靠,我用它做的任何事都不会改变它。

答案 2 :(得分:3)

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

答案 3 :(得分:2)

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

作为monads的简短非正式解释:

可以将Monads视为在程序中传递的数据+上下文。这是解释中经常使用的“太空服”。您将数据和上下文一起传递并将任何操作插入此Monad。一旦将数据插入到上下文中,通常就无法将数据恢复,您可以采用相反的方式进行插入操作,以便它们处理与上下文相结合的数据。通过这种方式,您似乎可以获得数据,但如果仔细观察,您就不会这样做。

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

答案 4 :(得分:2)

在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.