于是我开始换我周围的单子头(Haskell中使用)。 我很好奇什么其他方式IO或状态可在纯函数式语言处理(无论在理论上还是现实)。 例如,有一个名为使用“效应打字”“水银”的逻辑语言。 在一个程序,如哈斯克尔,怎么会影响打字的工作? 如何其他系统的工作?
Answer 1:
这里有涉及到几个不同的问题。
首先, IO
和State
是非常不同的事情。 State
是容易做到的自己:只是通过一个额外的参数传递给每个函数,并返回一个额外的结果,和你有一个“全状态的功能”; 例如,转a -> b
成a -> 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 s
和GetLine
; 我们得到(请求)值之后,我们可以检查(响应)列表中的下一个元素,而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
。
然而,当我们一起工作IO
和State
(和列表和功能, Maybe
和解析器和延续传递风格和...)足够长的时间,我们终于弄清楚,他们的行为非常相似,在某些方面。 我们可以写,打印每个字符串列表中的一个功能,并运行列表中的每一个状态的计算,并通过线程状态的功能,他们会看起来非常相似对方。
因为我们不喜欢写了很多的外观类似的代码,我们想办法给它抽象; Monad
原来是一个伟大的抽象,因为它让我们抽象的多种类型的看起来很不同,但仍然提供了很多有用的功能(包括一切Control.Monad
)。
鉴于bindIO :: IO a -> (a -> IO b) -> IO b
和returnIO :: a -> IO a
,我们可以写任何IO
在Haskell程序而不用考虑单子。 但是,我们很可能最终复制了大量的功能Control.Monad
,像mapM
和forever
和when
和(>=>)
通过实施共同的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,变成的表达式
x
到return 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的调用单独的操作(例如,一个副作用print
,getLine
等)。 但是,如果在你的语言(其中的右手侧的表达式的评价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操作,如
print
和getLine
,严格在World
参数(所以之前的操作首先运行,和侧effectful(所以I / O情况作为评价他们的副作用)再次,如果您已经评价过的副作用,在我看来这不是真正纯粹的功能。 - 让
World
类型实际上代表了I / O需要执行。 这有同样的问题GHC的IO
与尾递归程序执行。 假设我们改变的结果,main
以main 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操作的单子风格。