保持国家纯粹的功能语言

Ham*_*aya 10 haskell functional-programming clojure

我试图弄清楚如何做以下事情,假设你正在为一台直流电机控制器,你想让它以一定的速度旋转,由用户设定,


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

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


既然设定点可以随时通过Web应用程序进行更改,我想不出一种方法可以在不使用ref的情况下执行此操作,所以我的问题是函数式语言如何处理这类事情?(尽管这个例子是在clojure中我对一般的想法感兴趣.)

Jon*_*nas 15

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

Clojure不是一种函数式语言,在这种情况下,暂时将纯函数放在一边并用身份模拟系统的固有状态可能是个好主意.

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

我选择了一个atom但是根据你的要求,使用a ref或a更合适agent.

电机是您程序中的标识.对于某些在不同时间具有不同值的事物而言,这是一个"标签" ,这些值彼此相关(即电机的速度).我已经: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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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


C. *_*ann 8

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

和往常一样,Haskell中的答案有点玩笑,在这种情况下,可变数据(或带有副作用的任何东西)的访问结构是这样的,即类型系统保证它从内部"看起来"纯净,制作最终节目时会产生副作用.monad的常见业务是其中很大一部分,但细节并不重要,大多分散注意力.在实践中,它只是意味着您必须明确可能出现副作用的位置和顺序,并且不允许您"作弊".

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

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

有趣的是,整个段落直接转换为GHC Haskell.我猜测"Vars"等同于HaskellMVar,而"Refs"几乎肯定TVarstm包中的相同.

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

setPoint :: STM (TVar Int)
setPoint = newTVar 90
Run Code Online (Sandbox Code Playgroud)

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

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

在实际使用中,我的代码会比这简单得多,但我在这里留下了更加冗长的东西,希望不那么神秘.

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


LiK*_*Kao 2

为此所需的模式称为 Monad。如果您真的想进入函数式编程,您应该尝试了解 monad 的用途以及它们的功能。作为起点,我建议使用此链接

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

Monad 可以看作是在程序中传递的数据 + 上下文。这就是解说中经常使用的“宇航服”。您可以将数据和上下文一起传递,并将任何操作插入到此 Monad 中。一旦数据被插入到上下文中,通常就无法取回数据,您只能以相反的方式插入操作,以便它们结合上下文来处理数据。通过这种方式,看起来好像您已经将数据取出来了,但如果仔细观察,您却根本没有取出来。

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

  • 郑重声明一下,单子只是 FP 语言表示状态的几种方式之一。 (5认同)