还有什么其他办法能状态在纯函数式语言与单子处理之外?(What other ways can sta

2019-07-04 02:45发布

于是我开始换我周围的单子头(Haskell中使用)。 我很好奇什么其他方式IO或状态可在纯函数式语言处理(无论在理论上还是现实)。 例如,有一个名为使用“效应打字”“水银”的逻辑语言。 在一个程序,如哈斯克尔,怎么会影响打字的工作? 如何其他系统的工作?

Answer 1:

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

首先, IOState是非常不同的事情。 State是容易做到的自己:只是通过一个额外的参数传递给每个函数,并返回一个额外的结果,和你有一个“全状态的功能”; 例如,转a -> ba -> s -> (b,s)

有没有这里涉及到魔法: Control.Monad.State规定,使工作的包装与形式的“国家行动” s -> (a,s)方便,还有一堆的辅助功能,但仅此而已。

I / O,其性质,必须有在执行一些神奇。 但也有很多不涉及单词“单子”表达Haskell的I / O的方式。 如果我们有哈斯克尔的IO-无子集,是的,我们要从头开始创造IO,无需了解任何东西的单子,有很多事情我们可以做的。

例如,如果我们想要做的是打印到标准输出,我们可以说:

type PrintOnlyIO = String

main :: PrintOnlyIO
main = "Hello world!"

然后有一个RTS(运行时系统)评估串并打印。 这让我们写其I / O完全由印刷到stdout的任何一个Haskell程序。

这不是非常有用,但是,因为我们希望互动! 因此,让我们发明了一种新型的IO允许它。 我想到的最简单的事情是

type InteractIO = String -> String

main :: InteractIO
main = map toUpper

这种方法对IO让我们写一个从标准输入读取和写入到标准输出的任何代码(前奏带有一个功能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

最关键的是,而不是采取的回应是“懒列表”作为他开始的一个主要大吵,我们做的是接受一次一个说法个别要求。

我们的计划现在只是一个普通的数据类型-很像一个链表,但你不能只是遍历它通常为:当RTS解释main ,有时遇到像一个值GetLine ,其保持功能; 那么它必须使用RTS魔法stdin中获取一个字符串,该字符串传递给函数,然后才能继续。 练习:写interpret :: ContIO -> IO ()

需要注意的是,这些没有实现的涉及“世界传递”。 “世界传递”是不是真的怎么I / O工作在哈斯克尔。 在实际执行IO在GHC类型涉及内部类型,称为RealWorld ,但这只是一个实现细节。

实际哈斯克尔IO增加了一个类型参数,所以我们可以写入的动作是“生产”任意值-所以它看起来更像是data IO a = Done a | PutStr String (IO a) | GetLine (String -> IO a) | ... data IO a = Done a | PutStr String (IO a) | GetLine (String -> IO a) | ... data IO a = Done a | PutStr String (IO a) | GetLine (String -> IO a) | ... 。 这为我们提供了更大的灵活性,因为我们可以创造“ IO产生任意值的行动”。

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


哪里的单子进入它,然后呢? 事实证明,我们不需要Monad的I / O,而我们不需要Monad的状态,所以我们为什么需要它呢? 答案是,我们不知道。 有什么魔力类型类Monad

然而,当我们一起工作IOState (和列表和功能, Maybe和解析器和延续传递风格和...)足够长的时间,我们终于弄清楚,他们的行为非常相似,在某些方面。 我们可以写,打印每个字符串列表中的一个功能,并运行列表中的每一个状态的计算,并通过线程状态的功能,他们会看起来非常相似对方。

因为我们不喜欢写了很多的外观类似的代码,我们想办法给它抽象; Monad原来是一个伟大的抽象,因为它让我们抽象的多种类型的看起来很不同,但仍然提供了很多有用的功能(包括一切Control.Monad )。

鉴于bindIO :: IO a -> (a -> IO b) -> IO breturnIO :: a -> IO a ,我们可以写任何IO在Haskell程序而不用考虑单子。 但是,我们很可能最终复制了大量的功能Control.Monad ,像mapMforeverwhen(>=>)

通过实施共同的Monad API,我们得到的,因为我们做的解析器,并列出使用完全相同的代码与IO操作的工作。 这真的是我们拥有的唯一原因Monad类-捕捉不同类型之间的相似性。



Answer 2:

另一个主要方法是独特类型 ,如清洁 。 短篇小说是处理国家(包括现实世界)只能使用一次,并访问可变状态函数返回一个新的句柄。 这意味着,所述第一呼叫的输出是第二的输入时,迫使连续评估。

影响打字的使用弟子编译 Haskell的,但据我所知这将需要相当长的编译工作,以使其能够在,比方说,GHC。 我将离开的细节讨论那些更明智的比我自己。



Answer 3:

嗯,首先是什么状态呢? 它可以表现为一个可变的变量,你没有在Haskell有。 你只有内存引用(IOREF,无功,PTR等)和IO / ST行动来采取行动。

然而,国家本身可以是纯为好。 要承认,审查“流”类型:

data Stream a = Stream a (Stream a)

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

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

当你允许两个流通信。这变得有趣。 那么你得到的自动分类汽车:

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

这是真的很喜欢Stream ,除了现在在每一个瞬间流取得类型的一些输入值。 这形成了一个类别,所以流的一个瞬间可以从另一个流的同一时刻得到其数值。

同样有不同的解释的是:你有两种计算随时间变化的,你让他们进行沟通。 所以每次计算具有本地状态。 这里是一个类型,它是同构的Auto

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


Answer 4:

看看史哈斯克尔:懒惰带班 。 它描述了两种不同的方法在Haskell做I / O,单子被发明之前:延续和溪流。



Answer 5:

有称为官能反应性编程表示随时间变化的值和/或事件流作为第一类的抽象的方法。 这使我想起一个最近的例子是榆木 (它是写在Haskell和具有类似于哈斯克尔语法)。



Answer 6:

它不能是(如果没有通过“国家”你的意思是“I / O或类似的程序语言可变变量的行为”)。 首先,你要明白其中的可变变量或我使用单子/ O从何而来。 尽管大家普遍认为,一元I / O不是来自象Haskell语言,而是从语言,如ML。 欧亨尼奥·莫吉开发的原始单子边学习像ML 不纯的函数式语言的指称语义使用范畴论。 要知道为什么,认为(在Haskell)一个单子可以通过三个属性进行分类:

  • 之间的区别(在Haskell,类型的a )和表达式 (Haskell中,类型的IO a )。
  • 任何值可以(通过转换在Haskell,变成的表达式xreturn x )。
  • 超过值的任何函数(返回一个表达式)可以(通过计算在Haskell,可以适用于表达f =<< a )。

这些性质的明显真(至少)任何不纯功能语言的指称语义:

  • 一种表达 ,像print "Hello, world!\n" ,可以有副作用,但它的价值 ,如()不能。 因此,我们需要做两案中指称语义之间的区别。
  • 的值,如3 ,可用于任何需要的表达式。 因此,我们的指称语义需要一个函数把一个值的表达式。
  • 一个函数具有值作为参数(形式参数,以在严格的语言不具有副作用的函数),但也可以适用于一个表达式。 因此,我们需要一种方法来值的(表达式返回)功能,适用于表达式。

因此,对于不纯的功能(或程序)语言的任何指称语义将有一个单子的结构引擎盖下,即使该结构没有明确描述如何I / O工作中使用的语言。

什么纯粹的功能性的语言吗?

有在单纯的功能性语言做I / O的四种主要方式,我知道(实际上)(再次,限制我们自己的程序式I / O;玻璃钢是一个真正不同的模式):

  • 一元I / O
  • 延续
  • 唯一性/线性类型
  • 对话框

一元I / O是显而易见的。 继续基于I / O看起来是这样的:

main k = print "What is your name? " $
    getLine $ \ myName ->
    print ("Hello, " ++ myName ++ "\n") $
    k ()

每个I / O操作都需要“延续”,执行其操作,然后尾调用(引擎盖下)的延续。 所以在上面的程序:

  • print "What is your name? "运行,然后
  • getLine运行,然后
  • print ("Hello, " ++ myName ++ "\n")上运行,然后
  • k运行(其将控制返回给OS)。

延续单子是一个明显的语法改进上面。 更显著, 语义 ,我只能看到两种方式,使I / O实际上在上面的工作:

  • 使I / O操作(也延续)返回一个“I / O型”描述I / O要执行。 现在你有一个I / O单子(续单子为主)没有NEWTYPE包装。
  • 使I / O操作(也延续)返回的内容基本上是()然后执行I / O的调用单独的操作(例如,一个副作用printgetLine等)。 但是,如果在你的语言(其中的右手侧的表达式的评价main上面的定义是)是侧effectful,我不会考虑单纯的功能性。

怎么样的独特/线性类型? 这些使用特殊的“标记”值每个动作后,代表世界的状态,并执行测序。 代码如下所示:

main w0 = let
        w1 = print "What is your name? " w0
        (w2, myName) = getLine w1
        w3 = print $ "Hello, " ++ myName ++ "!\n"
    in w3

线性类型和独特类型之间的区别是,在直线型 ,结果又被w3 (它是类型的World ),而在独特类型 ,其结果可能是像w3 `seq` ()来代替。 w3只是有对I / O发生进行评估。

再次,状态单子是一个明显的改进句法于上述。 更显著, 语义 ,你再有两个选择:

  • 使I / O操作,如printgetLine ,严格在World参数(所以之前的操作首先运行,和侧effectful(所以I / O情况作为评价他们的副作用)再次,如果您已经评价过的副作用,在我看来这不是真正纯粹的功能。
  • World类型实际上代表了I / O需要执行。 这有同样的问题GHC的IO与尾递归程序执行。 假设我们改变的结果, mainmain w3 。 现在main尾调用本身。 任何功能尾通话本身,在纯粹的功能语言,没有值(仅仅是一个无限循环); 这是关于递归的指称语义是如何工作的单纯语言的一个基本事实。 同样,我也不会考虑打破了规则(尤其是像一个“特殊”的数据类型的任何语言World )是纯粹的功能。

所以,真的,唯一性或线性类型中)产生更清晰/清洁程序,如果你的状态单子将它们包装和b)实际上并没有办法做到的I / O在纯函数式语言毕竟。

什么对话? 这是做I / O的唯一途径(或者,在技术上,可变的变量,尽管这非常困难),真正的既纯粹的功能和独立的单子。 这看起来是这样的:

main resps = [
    PrintReq "What is your name? ",
    GetLineReq,
    PrintReq $ "Hello, " ++ myName ++ "!\n"
  ] where
    LineResp myName = resps !! 1

然而,你会发现这种方法的一些缺点:

  • 目前尚不清楚如何将I / O执行过程到这种方法。
  • 你必须使用数字或位置索引找到对应于给定的要求,这是相当脆弱的响应。
  • 有以规模刚刚超过它的接收之后的动作的响应没有明显的方式; 如果该程序以某种方式使用myName发出相应的前getLine要求,编译器会接受你的计划,但它会在运行时发生死锁。

一个简单的方法来解决所有这些问题是包装对话框中延续,就像这样:

type Cont = [Response] -> [Request]
print :: String -> Cont -> Cont
print msg k resps = PrintReq msg : case resps of
    PrintResp () : resps1 -> k resps1
getLine :: (String -> Cont) -> Cont
getLine k resps = GetLineReq : case resps of
    GetLineResp msg : resps1 -> k msg resps1

现在,该代码看起来相同,为延续传递approac代码I / O前面给出。 事实上,对话甚至在基于单子延续单子I / O系统,用于基于连续的I / O系统的延续良好的结果类型,或。 然而,通过转换回延续,同样的观点也适用,所以我们看到的是,即使在运行时系统采用对话框内, 程序仍然应该写入I / O操作的单子风格。



文章来源: What other ways can state be handled in a pure functional language besides with Monads?