除Monad外,还有哪些其他方式可以用纯粹的function语言来处理?

于是我开始把头围绕Monads(在Haskell中使用)。 我很好奇IO或状态可以用纯粹的function语言(理论上还是现实上)处理的其他方式。 例如,有一种叫做“水银”的逻辑语言,它使用“效应types”。 在像Haskell这样的程序中,效果types的工作将会如何? 其他系统如何工作?

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

首先, IOState是非常不同的东西。 State很容易做到:只要给每个函数传递一个额外的参数,并返回一个额外的结果,并且你有一个“有状态函数”。 例如,将a -> b转换a -> s -> (b,s)

这里没有什么魔法: Control.Monad.State提供了一个包装器,它使得s -> (a,s)formss -> (a,s) “状态动作”以及一堆帮助器函数的工作变得方便,但就是这样。

从本质上讲,I / O必须具有一些神奇的实现。 但是在Haskell中有很多expressionI / O的方式,不涉及单词“monad”。 如果我们现在有一个没有IO的Haskell子集,并且我们想从零开始发明IO,而不知道monad的任何事情,那么我们可能会做很多事情。

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

 type PrintOnlyIO = String main :: PrintOnlyIO main = "Hello world!" 

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

这不是很有用,但是,因为我们想要交互性! 所以让我们来发明一种新型的IO。 想到的最简单的事情是

 type InteractIO = String -> String main :: InteractIO main = map toUpper 

这种IO方法可以让我们编写任何从stdin读取并写入stdout的代码(顺便说一下,Prelude带有一个函数interact :: InteractIO -> IO () ,它会这样做)。

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

我们希望能够做比读取标准input和写入标准输出更多的function。 以下是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 

关键在于,我们不是在主要开始时把一个“懒惰列表”作为一个大的论据,而是一次一个地接受一个论点的个别请求。

我们的程序现在只是一个常规的数据types – 很像一个链表,除非你不能正常地遍历它:当RTS解释main ,有时会遇到像GetLine这样的值,它包含一个函数; 那么它必须从标准input使用RTS魔术获得一个string,并将该string传递给该函数,然后才能继续。 练习:编写interpret :: ContIO -> IO ()

请注意,这些实现都不涉及“全球通”。 “世界传递”并不是真正的I / O如何在Haskell中工作。 GHC中IOtypes的实际实现涉及一个名为RealWorld的内部types,但这只是一个实现细节。

实际的Haskell IO添加了一个types参数,所以我们可以编写“生成”任意值的操作 – 所以看起来更像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) | ... 这给了我们更多的灵活性,因为我们可以创build产生任意值的“ IO动作”。

(正如Russell O'Connor 指出的那样 ,这种types只是一个免费的monad,我们可以很容易地写出一个Monad实例。)


monads进来了,那么呢? 事实certificate,我们不需要Monad来处理I / O,而且我们也不需要Monad来处理状态,所以我们为什么需要呢? 答案是我们没有。 Monadtypes没有什么不可思议的。

然而,当我们与IOState (以及列表和函数, Maybe和parser以及continuation-passing风格等等)一起工作足够长时间时,我们最终发现它们在某些方面performance得非常相似。 我们可以编写一个函数来打印列表中的每一个string,还有一个函数可以在列表中运行每一个有状态的计算,并且通过状态进行线程化,而且看起来彼此非常相似。

既然我们不喜欢写很多类似的代码,我们想要一个抽象的方法; Monad是一个很好的抽象,因为它可以让我们抽象出很多不同的types,但是仍然提供了很多有用的function(包括Control.Monad所有function)。

给定bindIO :: IO a -> (a -> IO b) -> IO breturnIO :: a -> IO a ,我们可以在Haskell中编写任何IO程序,而不必考虑monad。 但是我们最终可能会复制Control.Monad的很多function,比如mapMforever以及when(>=>)

通过实现通用的Monad API,我们可以像使用parsing器和列表一样使用完全相同的代码来处理IO操作。 这是我们拥有Monad类的唯一原因 – 捕捉不同types之间的相似性。

另一个主要的方法是独特的打字 ,如在清洁 。 简言之,处理状态(包括现实世界)只能使用一次,而访问可变状态的函数返回一个新的句柄。 这意味着第一个调用的输出是第二个input,强制进行顺序评估。

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

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

但是,国家本身也可以是纯粹的。 承认审查“stream”types:

 data Stream a = Stream a (Stream a) 

这是价值stream。 然而,另一种解释这种types的方法是一个变化的价值:

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

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

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

这实际上就像Stream ,除了现在stream在每个时刻都得到一些types为a的input值。 这形成一个类别,因此一个stream的一个瞬间可以从另一个stream的同一时刻获得它的值。

再次对此有不同的解释:你有两个计算随着时间的推移而改变,你允许他们进行交stream。 所以每一个计算都有本地状态。 这是一个与Auto同构的types:

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

看看哈斯克尔的历史:懒惰和阶级 。 它描述了在monad发明之前做两种不同的在Haskell中做I / O的方法:continuations和streams。

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

它不可能是(不是通过“状态”,你的意思是“I / O或程序语言中的可变variables行为”)。 首先,你必须了解单variables或I / O的使用来自哪里。 尽pipestream行的观点认为,monadic I / O不是来自像Haskell这样的语言,而是来自像ML这样的语言。 欧金尼奥·莫吉(Eugenio Moggi)在研究将类别理论用于ML等不纯function语言的指称语义的同时,开发了原始单子 。 为了明白为什么,考虑一个monad(在Haskell中)可以分为三个属性:

  • (在Haskell中,types为a )和expression式 (在Haskell中,types为IO a )之间是有区别的。
  • 任何值都可以转换成一个expression式(在Haskell中,通过将x转换为return x )。
  • 任何超过值的函数(返回一个expression式)都可以应用到expression式中(在Haskell中,通过计算f =<< a )。

这些属性显然是(至less)任何不纯的function语言的指称语义:

  • 一个expression式 ,如print "Hello, world!\n" ,可能会有副作用,但是它的 ,比如() ,不能。 所以我们需要在指称语义上区分这两种情况。
  • 一个值,比如3 ,可以用在需要expression式的任何地方。 所以我们的指称语义需要一个函数来把一个值转换成一个expression式。
  • 函数将值作为参数(严格语言中函数的forms参数没有副作用),但可以应用于expression式。 所以我们需要一种方法来将(expression式返回)值的函数应用于expression式。

因此,对于不纯粹的function(或程序)语言的任何指称语义,都将具有一个monad的结构,即使这个结构没有明确地用于描述I / O如何在语言中工作。

纯粹的function语言呢?

在纯function语言中有四种主要的I / O方式,我知道(在实践中)(再一次,我们将自己限制在程序式I / O上; FRP真的是一种不同的范例):

  • Monadic I / O
  • 延续
  • 唯一性/线性types
  • 对话框

Monadic 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运行(将控制权交还给操作系统)。

对于上面的延续monad是一个明显的句法改进。 更重要的是,在语义上 ,我只能看到两种方法使I / O在上面实际工作:

  • 使I / O操作(和继续)返回描述要执行的I / O的“I / Otypes”。 现在你有一个I / O monad(继续monad-based)没有newtype包装。
  • 使I / O操作(和继续)返回基本上是()并执行I / O作为调用各个操作(例如printgetLine等)的副作用。 但是,如果用你的语言(上面main定义的右边)expression的评估是有副作用的,我不会认为这是纯粹的function。

唯一性/线性types呢? 这些使用特殊的“标记”值来表示每个操作之后的世界状态,并执行sorting。 代码如下所示:

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

线性types和唯一性types之间的区别在于,在线性types中,结果必须是w3 (它必须是Worldtypes),而在唯一性types中 ,结果可以是类似w3 `seq` ()w3只是要评估I / O的发生。

再一次,国家monad是上述明显的句法改进。 更重要的是,在语义上 ,你又有两个select:

  • World参数中严格执行printgetLine等I / O操作(所以前面的操作是先运行的,而且是副作用的(所以I / O是评估它们的副作用)。你有评价的副作用,在我看来,这不是真的纯粹的function。
  • 使Worldtypes实际上代表需要执行的I / O。 这与使用尾recursion程序的GHC的IO实现具有相同的问题。 假设我们把main w3的结果改成main w3 。 现在main尾巴呼叫本身。 任何以纯粹的function性语言来调用自身的函数都没有价值(只是一个无限循环)。 这是关于recursion的指称语义如何以纯语言工作的基本事实。 再次,我不会认为任何违反这个规则的语言(特别是像“ World ”这样的“特殊”数据types)纯粹是function性的。

所以,真正的唯一性或线性typesa)生成的程序,如果你将它们包装在单态中,那么程序就会更清晰/清晰,而且b)实际上并不是以纯粹function语言来做I / O的方式。

那对话呢? 这是做I / O(或技术上可变的variables,尽pipe这更难)的唯一方法,它确实是纯粹的function性和独立于monad的。 这看起来像这样:

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

但是,您会注意到这种方法的一些缺点:

  • 目前还不清楚如何将I / O执行程序纳入这种方法。
  • 您必须使用数字或位置索引来查找与给定请求相对应的响应,这非常脆弱。
  • 没有什么明显的方法可以对收到的行为进行回应, 如果这个程序在发出相应的getLine请求之前以某种方式使用了myName ,那么编译器会接受你的程序,但是它会在运行时死锁。

解决所有这些问题的一个简单方法就是将对话框包装起来,如下所示:

 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 

代码现在看起来与前面给出的继续传递给I / O的代码相同。 事实上,对于继续的I / O系统,甚至是基于monad的单点I / O系统,对话是一个很好的结果types。 然而,通过转换回延续,同样的理由适用,所以我们看到,即使运行时系统在内部使用对话框,仍然应该编写程序以一元风格执行I / O。