我正在试图弄清楚如何执行以下操作,假设您正在使用直流电机的控制器,以保持其以用户设定的特定速度旋转,
(def set-point (ref {:sp 90}))
(while true
(let [curr (read-speed)]
(controller @set-point curr)))
既然设置点可以随时通过web改变应用程序,我想不出一种方法可以在不使用ref的情况下做到这一点,所以我的问题是函数式语言如何处理这类事情? (尽管这个例子是在clojure中我对一般的想法感兴趣。)
答案 0 :(得分:15)
这不会回答你的问题,但我想说明这些事情是如何在Clojure中完成的。它可能有助于某人稍后阅读,因此他们认为他们不必阅读monad,反应性编程或其他“复杂”主题来使用Clojure。
Clojure不是一种纯粹的函数式语言,在这种情况下,暂时将纯函数放在一边并模型the inherent state of the system with identities可能是个好主意。
在Clojure中,您可能会使用其中一种引用类型。有几种可供选择,并且知道使用哪一种可能很困难。好消息是它们都支持统一更新模型,因此以后更改引用类型应该非常简单。
我选择了atom
,但根据您的要求,使用ref
或agent
可能更合适。
电机是程序中的标识。它是某些 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.