【问题标题】:What other ways can state be handled in a pure functional language besides with Monads?除了使用 Monads 之外,还有哪些其他方法可以用纯函数式语言处理状态?
【发布时间】:2012-11-12 06:31:29
【问题描述】:

所以我开始围绕 Monads(用于 Haskell)。我很好奇 IO 或状态还有哪些其他方式可以用纯函数式语言(理论上或现实中)来处理。例如,有一种名为“mercury”的逻辑语言使用“效果类型”。在像 haskell 这样的程序中,效果类型是如何工作的?其他系统如何工作?

【问题讨论】:

  • 正如 isturdy 所说,Clean 像 Mercury 一样使用唯一性类型,尽管这些 IMO 更适合 Mercury 的范式,而不是纯函数式语言,其中 Monad 只是要走的路。
  • 我想这更适合programmers.stackexchange.com
  • 为了纠正这个问题,Mercury 不使用效果类型,而是使用唯一性类型(如 Clean)。效果类型不同(参见 DDC)

标签: haskell functional-programming mercury


【解决方案1】:

这里涉及到几个不同的问题。

首先,IOState 是非常不同的东西。 State 很容易做到 你自己:只需向每个函数传递一个额外的参数,并返回一个额外的 结果,你有一个“有状态的功能”;例如,将a -> b 变成 a -> s -> (b,s).

这里没有魔法:Control.Monad.State 提供了一个包装器 也可以方便地使用 s -> (a,s) 形式的“状态操作” 作为一堆辅助函数,仅此而已。

I/O 就其本质而言,在其实现中必须具有一些魔力。但这有 很多在 Haskell 中表达 I/O 的方式都不涉及“monad”这个词。 如果我们有一个没有 IO 的 Haskell 子集,并且我们想从 从头开始,在不了解 monad 的情况下,我们可能会做很多事情 做。

例如,如果我们只想打印到标准输出,我们可能会说:

type PrintOnlyIO = String

main :: PrintOnlyIO
main = "Hello world!"

然后有一个 RTS(运行时系统)来评估字符串并打印它。 这让我们可以编写任何 I/O 完全由打印组成的 Haskell 程序 到标准输出。

然而,这并不是很有用,因为我们需要交互性!所以让我们发明 一种允许它的新型IO。想到的最简单的事情是

type InteractIO = String -> String

main :: InteractIO
main = map toUpper

这种 IO 方法让我们可以编写任何从标准输入读取并写入 stdout (Prelude 自带函数interact :: InteractIO -> IO () 顺便说一句,它是这样做的)。

这要好得多,因为它可以让我们编写交互式程序。但它是 与我们想要做的所有 IO 相比,仍然非常有限,而且相当 容易出错(如果我们不小心尝试将标准输入读得太远,程序 只会阻塞,直到用户输入更多内容)。

我们希望能够做的不仅仅是读取标准输入和写入标准输出。这是如何做 早期版本的 Haskell 进行 I/O,大致如下:

data Request = PutStrLn String | GetLine | Exit | ...
data Response = Success | Str String | ...
type DialogueIO = [Response] -> [Request]

main :: DialogueIO
main resps1 =
    PutStrLn "what's your name?"
  : GetLine
  : case resps1 of
        Success : Str name : resps2 ->
            PutStrLn ("hi " ++ name ++ "!")
          : Exit

当我们写main 时,我们得到一个惰性列表参数并返回一个惰性列表作为 结果。我们返回的惰性列表具有PutStrLn sGetLine 之类的值; 在我们产生一个(请求)值之后,我们可以检查下一个元素 (响应)列表,RTS 将安排它作为对我们的响应 请求。

有一些方法可以更好地使用这种机制,但你可以 想象一下,这种方法很快就会变得很尴尬。还有就是 和上一个一样容易出错。

这是另一种不太容易出错的方法,并且在概念上非常 接近 Haskell IO 的实际行为方式:

data ContIO = Exit | PutStrLn String ContIO | GetLine (String -> ContIO) | ...

main :: ContIO
main =
    PutStrLn "what's your name?" $
    GetLine $ \name ->
    PutStrLn ("hi " ++ name ++ "!") $
    Exit

关键是不要将响应的“惰性列表”作为一个大 在 main 开头的参数,我们提出接受一个的个别请求 一次争论。

我们的程序现在只是一个常规数据类型——很像一个链表,除了 你不能只是正常遍历它:当 RTS 解释 main 时,有时 它遇到像GetLine 这样的值,其中包含一个函数;那么它必须得到 使用 RTS 魔法从标准输入获取字符串,并将该字符串传递给函数, 在它可以继续之前。练习:写interpret :: ContIO -> IO ()

请注意,这些实现都不涉及“世界传递”。 “世界传递”并不是 Haskell 中 I/O 的真正工作方式。实际上 GHC 中IO 类型的实现涉及一个名为的内部类型 RealWorld,但这只是一个实现细节。

实际的 Haskell IO 添加了一个类型参数,因此我们可以编写如下操作 “产生”任意值——所以它看起来更像data IO a = Done a | PutStr String (IO a) | GetLine (String -> IO a) | ...。这给了我们更多 灵活性,因为我们可以创建产生任意的“IO 动作” 价值观。

(作为罗素奥康纳points out, 这种类型只是一个免费的单子。我们可以很容易地为它编写一个Monad 实例。)


那么,monad 是从哪里来的呢?事实证明,我们不需要Monad I/O,我们不需要Monad 来表示状态,那么为什么我们需要它呢?这 答案是我们没有。类型类 Monad 没有什么神奇之处。

但是,当我们使用 IOState(以及列表和函数以及 Maybe 和解析器和继续传递风格和......)足够长的时间,我们 最终发现它们在某些方面的行为非常相似。我们可能 编写一个打印列表中每个字符串的函数,以及一个运行的函数 列表中的每个有状态计算并通过线程处理状态,它们将 看起来非常相似。

由于我们不喜欢编写很多看起来相似的代码,我们想要一种方法来 抽象它; Monad 原来是一个很棒的抽象,因为它让我们 抽象了许多看起来非常不同,但仍然提供了很多有用的类型 功能(包括Control.Monad 中的所有内容)。

鉴于bindIO :: IO a -> (a -> IO b) -> IO breturnIO :: a -> IO a,我们 可以在 Haskell 中编写任何 IO 程序,而无需考虑单子。但 我们可能最终会复制Control.Monad 中的许多功能, 比如mapMforeverwhen(>=>)

通过实现通用的Monad API,我们可以使用完全相同的代码 像使用解析器和列表一样使用 IO 操作。这真的是唯一的 我们有Monad 类的原因——捕捉它们之间的相似之处 不同的类型。

【讨论】:

  • "给定 bindIO :: IO a -> (a -> IO b) -> IO b 和 returnIO :: a -> IO a,我们可以不用考虑用 Haskell 编写任何 IO 程序单子。”不正确:这些函数创建了一个 monad。我们也许可以在不考虑类型类 Monad 的情况下编写一个 IO 程序,但是具有这些签名的函数(只要它们遵守一些我假设您的假设函数会遵循的进一步定律)构成是一个 monad。跨度>
  • 当然。我的意思是不考虑一般的单子。每次我们使用(.),它都是fmap的有效实现,它遵守Functor的规律;但这并不意味着我们在使用函子时通常会考虑函子的属性。只有当我们认识到抽象时,我们才能得到好处。
  • 这是对Haskell中State、IO和Monads的很好的描述,但只是间接回答了用户的问题。
  • shachaf:在FRP 的提示下,IO 类型可以简化为data IO a = Return a | Proceed (Time -> IO a),其中Time 是抽象的。然后可以用 done = Return 等定义替换 DonePutStrGetLine et al 等构造函数的队列以及例如putStr = Proceed . primPutStrgetLine = Proceed primGetLine,给定了 primPutStrprimGetLine 的适当声明。
【解决方案2】:

另一个主要方法是uniqueness typing,如Clean。简短的故事是状态句柄(包括现实世界)只能使用一次,访问可变状态的函数返回一个新句柄。这意味着第一次调用的输出是第二次调用的输入,强制执行顺序评估。

在 Haskell 的 Disciple Compiler 中使用了效果类型,但据我所知,在 GHC 中启用它需要大量的编译器工作。我将把细节的讨论留给那些比我更了解的人。

【讨论】:

  • Disciple Compiler 不编译 Haskell 代码,它编译 Disciple 代码,这是一种不同但相关的语言。这不能集成到 GHC 中,因为效果类型不是 Haskell 的一部分。
  • 它自己的文档声称它是 Haskell 的一种方言,我愿意承认这一点。它似乎与 GHC 及其众多扩展没有什么不同,尽管更难与不使用扩展的代码集成。
【解决方案3】:

首先,什么是状态?它可以表现为一个可变变量,这是 Haskell 中没有的。您只有内存引用(IORef、MVar、Ptr 等)和 IO/ST 操作来对它们进行操作。

但是,状态本身也可以是纯的。要确认查看“流”类型:

data Stream a = Stream a (Stream a)

这是一个值流。然而,解释这种类型的另一种方法是改变值:

stepStream :: Stream a -> (a, Stream a)
stepStream (Stream x xs) = (x, xs)

当您允许两个流进行通信时,这会变得很有趣。然后你会得到自动机类别 Auto:

newtype Auto a b = Auto (a -> (b, Auto a b))

这真的很像Stream,只是现在流在每一刻都会得到一些a类型的输入值。这形成了一个类别,因此流的一个瞬间可以从另一个流的同一瞬间获取其值。

再次对此进行不同的解释:您有两个随时间变化的计算,并且您允许它们进行通信。所以每个计算都有本地状态。这是一个与Auto 同构的类型:

data LS a b =
    forall s.
    LS s ((a, s) -> (b, s))

【讨论】:

    【解决方案4】:

    看看A History of Haskell: Being Lazy With Class。它描述了在 monad 被发明之前在 Haskell 中进行 I/O 的两种不同方法:延续和流。

    【讨论】:

      【解决方案5】:

      有一种称为函数响应式编程的方法,它将时变值和/或事件流表示为一流的抽象。我最近想到的一个例子是Elm(它是用 Haskell 编写的,语法类似于 Haskell)。

      【讨论】:

        【解决方案6】:

        我很好奇 - 在纯函数式语言中还有哪些其他方式可以处理 I/O 或状态(理论上或现实中)?

        我只是补充一下这里已经提到的内容(注意:其中一些方法似乎没有,所以有一些“即兴名字”)。

        具有免费描述或实现的方法:

        其他方法 - 仅供参考:

        • 系统令牌

          L.奥古斯特森。使用系统令牌的功能性 I/O。 PMG 备忘录 72,查尔姆斯理工大学计算机科学系,S-412 96 Göteborg,1989 年。

        • “效果树”

          Rebelsky S.A. (1992) I/O 树和交互式惰性函数式编程。在:Bruynooghe M., Wirsing M. (eds) 编程语言实现和逻辑编程。 PLILP 1992。计算机科学讲义,第 631 卷。Springer,柏林,海德堡。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2018-04-21
          • 2012-04-10
          • 1970-01-01
          • 2018-03-01
          • 1970-01-01
          • 2011-05-12
          • 2015-12-08
          相关资源
          最近更新 更多