用纯函数式语言保持状态
我正在尝试弄清楚如何执行以下操作,假设您正在开发直流电机的控制器,您希望让它以用户设置的特定速度旋转,
(def set-point (ref {:sp 90}))
(while true
(let [curr (read-speed)]
(controller @set-point curr)))
现在该设定点可以通过web 应用程序,我想不出一种不使用 ref 来做到这一点的方法,所以我的问题是函数式语言如何处理这类事情? (尽管这个例子是在 clojure 中的,但我对总体思路很感兴趣。)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
这不会回答你的问题,但我想展示这些事情是如何在 Clojure 中完成的。它可能会对稍后阅读本文的人有所帮助,这样他们就不需要阅读 monad、反应式编程或其他“复杂”主题来使用 Clojure。
Clojure 不是一种纯函数式语言,在这种情况下,暂时将纯函数放在一边并模型 具有身份的系统的固有状态。
在 Clojure 中,您可能会使用其中一种引用类型。有多种可供选择,并且知道使用哪一种可能很困难。好消息是它们都支持统一更新模型,因此稍后更改引用类型应该非常简单。
我选择了
atom
但根据您的要求,使用它可能更合适ref
或代理
。电机是程序中的一个标识。它是某些事物的“标签”,在不同时间具有不同的值,并且这些值彼此相关(即电机的速度)。我在原子上放置了一个
:validator
以确保速度永远不会低于零。如果您想更改电机标识的语义,您至少有两种其他参考类型可供选择。
如果你想异步改变电机的速度,你可以使用代理。然后您需要将
swap!
更改为send
。例如,如果调整电机速度的客户端与使用电机速度的客户端不同,那么“最终”更改速度就可以了。另一种选择是使用
ref
,如果电机需要与系统中的其他身份进行协调,这将是合适的。如果您选择此引用类型,则可以使用alter
更改swap!
。此外,所有状态更改都在带有dosync
的事务中运行,以确保事务中的所有身份都以原子方式更新。在 Clojure 中不需要 Monad 来建模身份和状态!
This will not answer your question but I want to show how these things are done in Clojure. It might help someone reading this later so they don't think they have to read up on monads, reactive programming or other "complicated" subjects to use Clojure.
Clojure is not a purely functional language and in this case it might be a good idea to leave the pure functions aside for a moment and model the inherent state of the system with identities.
In Clojure, you would probably use one of the reference types. There are several to choose from and knowing which one to use might be difficult. The good news is they all support the unified update model so changing the reference type later should be pretty straight forward.
I've chosen an
atom
but depending on your requirements it might be more appropriate to use aref
or anagent
.The motor is an identity in your program. It is a "label" for some thing that has different values at different times and these values are related to each other (i.e., the speed of the motor). I have put a
:validator
on the atom to ensure that the speed never drops below zero.If you want to change the semantics of the motor identity you have at least two other reference types to choose from.
If you want to change the speed of the motor asynchronously you would use an agent. Then you need to change
swap!
withsend
. This would be useful if, for example, the clients adjusting the motor speed are different from the clients using the motor speed, so that it's fine for the speed to be changed "eventually".Another option is to use a
ref
which would be appropriate if the motor need to coordinate with other identities in your system. If you choose this reference type you changeswap!
withalter
. In addition, all state changes are run in a transaction withdosync
to ensure that all identities in the transaction are updated atomically.Monads are not needed to model identities and state in Clojure!
对于这个答案,我将把“纯粹的函数式语言”解释为“一种排除副作用的 ML 风格语言”,我将其解释为“Haskell”,我将其解释为“GHC” 。这些都不是严格正确的,但考虑到你将其与 Lisp 衍生物进行对比,并且 GHC 相当突出,我猜这仍然是你问题的核心。
与往常一样,Haskell 中的答案是一些技巧,其中对可变数据(或任何具有副作用的数据)的访问是以这样一种方式构建的,即类型系统保证它从内部“看起来”是纯粹的,同时生成的最终程序会产生预期的副作用。与 monad 相关的常见事务占了很大一部分,但细节并不重要,而且大多会分散人们对问题的注意力。在实践中,这只是意味着你必须明确副作用可能发生的位置和顺序,并且不允许你“作弊”。
可变性原语通常由语言运行时提供,并通过在运行时也提供的某些 monad 中生成值的函数进行访问(通常是 IO,有时是更专门的)。首先,让我们看一下您提供的 Clojure 示例:它使用
ref
,此处的文档:有趣的是,整个段落非常直接地翻译成 GHC Haskell。我猜“Vars”相当于 Haskell 的
MVar
,而“Refs”几乎肯定相当于TVar
位于stm
包 中。因此,要将示例转换为 Haskell,我们需要一个创建
TVar
的函数:...我们可以在如下代码中使用它:
在实际使用中,我的代码将比那个,但我在这里留下了更详细的内容,希望不那么神秘。
我想有人可能会反对说这段代码不是纯粹的并且使用的是可变状态,但是......那又怎样呢?在某个时刻,程序将运行,我们希望它进行输入和输出。重要的是,我们保留了纯代码的所有好处,即使在使用它来编写具有可变状态的代码时也是如此。例如,我使用
repeat
函数实现了副作用的无限循环;但repeat
仍然是纯粹的并且行为可靠,我对它所做的任何事情都不会改变这一点。For this answer, I'm going to interpret "a purely functional language" as meaning "an ML-style language that excludes side effects" which I will interpret in turn as meaning "Haskell" which I'll interpret as meaning "GHC". None of these are strictly true, but given that you're contrasting this with a Lisp derivative and that GHC is rather prominent, I'm guessing this will still get at the heart of your question.
As always, the answer in Haskell is a bit of sleight-of-hand where access to mutable data (or anything with side effects) is structured in such a way that the type system guarantees that it will "look" pure from the inside, while producing a final program that has side effects where expected. The usual business with monads is a large part of this, but the details don't really matter and mostly distract from the issue. In practice, it just means you have to be explicit about where side effects can occur and in what order, and you're not allowed to "cheat".
Mutability primitives are generally provided by the language runtime, and accessed through functions that produce values in some monad also provided by the runtime (often
IO
, sometimes more specialized ones). First, let's take a look at the Clojure example you provided: it usesref
, which is described in the documentation here:Amusingly, that whole paragraph translates pretty directly to GHC Haskell. I'm guessing that "Vars" are equivalent to Haskell's
MVar
, while "Refs" are almost certainly equivalent toTVar
as found in thestm
package.So to translate the example to Haskell, we'll need a function that creates the
TVar
:...and we can use it in code like this:
In actual use my code would be far more terse than that, but I've left things more verbose here in hopes of being less cryptic.
I suppose one could object that this code isn't pure and is using mutable state, but... so what? At some point a program is going to run and we'd like it to do input and output. The important thing is that we retain all the benefits of code being pure, even when using it to write code with mutable state. For instance, I've implemented an infinite loop of side effects using the
repeat
function; butrepeat
is still pure and behaves reliably and nothing I can do with it will change that.一种以功能性方式解决显然需要可变性(例如 GUI 或 Web 应用程序)的问题的技术是 函数式响应式编程。
A technique to tackle problems that apparently scream for mutability (like GUI or web applications) in a functional way is Functional Reactive Programming.
为此所需的模式称为 Monad。如果您真的想进入函数式编程,您应该尝试了解 monad 的用途以及它们的功能。作为起点,我建议此链接。
作为对 monad 的简短非正式解释:
Monad 可以被视为在程序中传递的数据 + 上下文。这就是解说中经常使用的“宇航服”。您可以将数据和上下文一起传递,并将任何操作插入到此 Monad 中。一旦数据被插入到上下文中,通常就无法取回数据,您只能以相反的方式插入操作,以便它们结合上下文来处理数据。通过这种方式,看起来好像您已经将数据取出来了,但如果仔细观察,您却根本没有取出来。
根据您的应用程序,上下文几乎可以是任何内容。结合多个实体、异常、选项或现实世界(i/o-monad)的数据结构。在上面链接的论文中,上下文将是算法的执行状态,因此这与您想到的事情非常相似。
The pattern you need for this is called Monads. If you really want to get into functional programming you should try to understand what monads are used for and what they can do. As a starting point I would suggest this link.
As a short informal explanation for monads:
Monads can be seen as data + context that is passed around in your program. This is the "space suit" often used in explanations. You pass data and context around together and insert any operation into this Monad. There is usually no way to get the data back once it is inserted into the context, you just can go the other way round inserting operations, so that they handle data combined with context. This way it almost seems as if you get the data out, but if you look closely you never do.
Depending on your application the context can be almost anything. A datastructure that combines multiple entities, exceptions, optionals, or the real world (i/o-monads). In the paper linked above the context will be execution states of an algorithm, so this is quite similar to the things you have in mind.
在 Erlang 中,您可以使用进程来保存该值。像这样的东西:
In Erlang you could use a process to hold the value. Something like this: