问题描述
所以我开始把头围绕Monads(在Haskell中使用)。我很好奇IO或状态可以用纯粹的功能语言(理论上还是现实上)处理的其他方式。例如,有一种称为汞的逻辑语言,它使用效果类型。在诸如haskell的程序中,效果类型工作将如何?其他系统如何工作?这里涉及到几个不同的问题。 IO
和 State
是完全不同的东西。 State
很容易做到
:只需向每个函数传递一个额外的参数,并返回一个额外的
结果,并且您有一个有状态函数;例如,转到 a - > b
转换为
a - > s - > (b,s)
。
这里没有任何魔法: Control.Monad.State
提供了一个封装器,
使得 s - >形式的状态动作 (a,s)
方便,以及
作为一堆辅助函数,但就是这样。
I / O,就其性质而言,其实施必须具有一定的魔力。但是有很多表达I / O的方法在Haskell中并不涉及单词monad。
如果我们现在有一个没有IO的Haskell子集,并且我们想从
scratch开发IO,但不知道monad的任何内容,我们可能会做很多事情来做
。 / p>
例如,如果我们想要做的只是打印到stdout,我们可能会说:
键入PrintOnlyIO =字符串
main :: PrintOnlyIO
main =Hello world!
然后有一个RTS(运行时系统)来评估字符串并打印它。
这让我们编写任何Haskell程序,其I / O完全由打印
到标准输出。
然而,这并不是非常有用,因为我们想要交互性!所以让我们来创造一个新的IO类型
,它允许它。想到最简单的事情是:
type InteractIO = String - >字符串
main :: InteractIO
main =映射toUpper
这种IO方法让我们可以编写任何从stdin读取并写入
stdout的代码(Prelude带有一个函数 interact :: InteractIO - > IO()
,顺便说一句)。
这样好多了,因为它可以让我们编写交互式程序。但是,与所有我们想要做的IO相比,它仍然非常有限,并且还有相当多的
容易出错(如果我们不小心尝试读入stdin太远,程序
将会阻止直到用户输入更多)。
我们希望能够做的不仅仅是读取标准输入和写入标准输出。以下是
早期版本的Haskell做了I / O的情况:大约: GetLine |退出| ...
data Response =成功| Str String | ...
type DialogueIO = [Response] - > [请求]
main :: DialogueIO
main resps1 =
PutStrLn你叫什么名字?
:GetLine
:$ b $的案例resps1成功:str名称:resps2 - >
PutStrLn(hi++ name ++!)
:退出
当我们编写 main
时,我们得到一个懒惰的列表参数,并返回一个懒惰列表作为
结果。我们返回的惰性列表的值为 PutStrLn s
和 GetLine
;
之后,我们可以检查
(response)列表的下一个元素,RTS将安排它作为对
请求的响应。 / p>
有很多方法可以使这个机制更好地工作,但正如您可以想象的那样,这种方法很快就会变得非常尴尬。此外,它的错误倾向与前一种方法相同。
下面是另一种方法,它的错误率很低,概念上非常低
接近Haskell IO的实际行为:
data ContIO =退出| PutStrLn字符串Cont | | GetLine(字符串 - > ContIO)| ...
main :: ContIO
main =
PutStrLn你叫什么名字? $
GetLine $ \\\
ame - >
PutStrLn(hi++ name ++!)$
退出
关键在于,我们并不是在主开始时将一个懒惰列表作为一个大的
参数,而是一次接受一个
参数的单个请求。
我们的程序现在只是一个常规的数据类型 - 很像链接列表,除了
,您不能只是正常地遍历它:当RTS解释 main
,有时是
,它遇到一个像 GetLine
这样的值,它包含一个函数;那么它必须使用RTS魔术从标准输入中获得
字符串,并在该字符串可以继续之前将该字符串传递给函数
。练习:编写 interpret :: ContIO - > IO()
。
请注意,这些实现都不涉及世界传递。
世界传递并不是真正的I / O如何在Haskell中工作。 GHC中 IO
类型的实际
实现涉及一个名为
RealWorld
的内部类型,但这只是一个实现细节。
实际Haskell IO
添加了一个类型参数,因此我们可以编写这样的动作:
产生任意值 - 因此它看起来更像是数据IO a =完成a |
。这给了我们更多的
PutStr字符串(IO a)| GetLine(字符串 - > IO a)| ......
灵活性,因为我们可以创建产生任意
值的 IO
行动。
(正如Russell O'Connor ,这种类型的
是只是一个免费的monad,我们可以很容易地为它写一个 Monad
实例。)
monads在哪里进入呢?事实证明,对于
I / O,我们不需要 Monad
,并且我们不需要 Monad $ c $对于国家,为什么我们需要它呢?
的答案是我们没有。对于类型 Monad
。
没有什么不可思议的。然而,当我们使用 IO
和 State
(以及列表和函数和
也许
以及解析器和延续 - 持续的风格和......)足够长的时间,我们
最终发现他们在某些方面表现非常相似。我们可能
编写一个函数打印列表中的每一个字符串,并且一个函数在列表中运行
每个有状态计算并且通过状态线程,并且
看起来非常类似彼此。
因为我们不喜欢写很多类似外观的代码,所以我们想要一种方法来
抽象它; Monad
事实证明是一个很好的抽象,因为它让我们
抽象出很多类似的东西,但它们仍然提供了很多有用的
功能(包括 Control.Monad
中的所有内容)。
给定 bindIO :: IO a - > (a→IO b)→> IO b
和 returnIO :: a - > IO a
,我们
可以在Haskell中编写任何 IO
程序,而不必考虑monad。但是
可能最终会复制 Control.Monad
,
中的很多函数,如 mapM $当< / code>和
(> =>)时,c $ c>和
forever
code $。
通过实现常见的 Monad
API,我们可以使用完全相同的代码
与IO操作一起使用,就像我们对解析器和列表所做的一样。这是我们获得 Monad
类的唯一
原因,以捕获
不同类型之间的相似性。
So I started to wrap my head around Monads (used in Haskell). I'm curious what other ways IO or state can be handled in a pure functional language (both in theory or reality). For example, there is a logical language called "mercury" that uses "effect-typing". In a program such as haskell, how would effect-typing work? How does other systems work?
There are several different questions involved here.
First, IO
and State
are very different things. State
is easy to doyourself: Just pass an extra argument to every function, and return an extraresult, and you have a "stateful function"; for example, turn a -> b
intoa -> s -> (b,s)
.
There's no magic involved here: Control.Monad.State
provides a wrapper thatmakes working with "state actions" of the form s -> (a,s)
convenient, as wellas a bunch of helper functions, but that's it.
I/O, by its nature, has to have some magic in its implementation. But there area lot of ways of expressing I/O in Haskell that don't involve the word "monad".If we had an IO-free subset of Haskell as-is, and we wanted to invent IO fromscratch, without knowing anything about monads, there are many things we mightdo.
For example, if all we want to do is print to stdout, we might say:
type PrintOnlyIO = String
main :: PrintOnlyIO
main = "Hello world!"
And then have an RTS (runtime system) which evaluates the string and prints it.This lets us write any Haskell program whose I/O consists entirely of printingto stdout.
This isn't very useful, however, because we want interactivity! So let's inventa new type of IO which allows for it. The simplest thing that comes to mind is
type InteractIO = String -> String
main :: InteractIO
main = map toUpper
This approach to IO lets us write any code which reads from stdin and writes tostdout (the Prelude comes with a function interact :: InteractIO -> IO ()
which does this, by the way).
This is much better, since it lets us write interactive programs. But it'sstill very limited compared to all the IO we want to do, and also quiteerror-prone (if we accidentally try to read too far into stdin, the programwill just block until the user types more in).
We want to be able to do more than read stdin and write stdout. Here's howearly versions of Haskell did I/O, approximately:
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
When we write main
, we get a lazy list argument and return a lazy list as aresult. The lazy list we return has values like PutStrLn s
and GetLine
;after we yield a (request) value, we can examine the next element of the(response) list, and the RTS will arrange for it to be the response to ourrequest.
There are ways to make working with this mechanism nicer, but as you canimagine, the approach gets pretty awkward pretty quickly. Also, it'serror-prone in the same way as the previous one.
Here's another approach which is much less error-prone, and conceptually veryclose to how Haskell IO actually behaves:
data ContIO = Exit | PutStrLn String ContIO | GetLine (String -> ContIO) | ...
main :: ContIO
main =
PutStrLn "what's your name?" $
GetLine $ \name ->
PutStrLn ("hi " ++ name ++ "!") $
Exit
The key is that instead of taking a "lazy list" of responses as one bigargument at he beginning of main, we make individual requests that accept oneargument at a time.
Our program is now just a regular data type -- a lot like a linked list, exceptyou can't just traverse it normally: When the RTS interprets main
, sometimesit encounters a value like GetLine
which holds a function; then it has to geta string from stdin using RTS magic, and pass that string to the function,before it can continue. Exercise: Write interpret :: ContIO -> IO ()
.
Note that none of these implementations involve "world-passing"."world-passing" isn't really how I/O works in Haskell. The actualimplementation of the IO
type in GHC involves an internal type calledRealWorld
, but that's only an implementation detail.
Actual Haskell IO
adds a type parameter so we can write actions that"produce" arbitrary values -- so it looks more like data IO a = Done a |PutStr String (IO a) | GetLine (String -> IO a) | ...
. That gives us moreflexibility, because we can create "IO
actions" that produce arbitraryvalues.
(As Russell O'Connor points out,this type is just a free monad. We can write a Monad
instance for it easily.)
Where do monads come into it, then? It turns out that we don't need Monad
forI/O, and we don't need Monad
for state, so why do we need it at all? Theanswer is that we don't. There's nothing magical about the type class Monad
.
However, when we work with IO
and State
(and lists and functions andMaybe
and parsers and continuation-passing style and ...) for long enough, weeventually figure out that they behave pretty similarly in some ways. We mightwrite a function that prints every string in a list, and a function that runsevery stateful computation in a list and threads the state through, and they'lllook very similar to each other.
Since we don't like writing a lot of similar-looking code, we want a way toabstract it; Monad
turns out to be a great abstraction, because it lets usabstract many types that seem very different, but still provide a lot of usefulfunctionality (including everything in Control.Monad
).
Given bindIO :: IO a -> (a -> IO b) -> IO b
and returnIO :: a -> IO a
, wecould write any IO
program in Haskell without ever thinking about monads. Butwe'd probably end up replicating a lot of the functions in Control.Monad
,like mapM
and forever
and when
and (>=>)
.
By implementing the common Monad
API, we get to use the exact same code forworking with IO actions as we do with parsers and lists. That's really the onlyreason we have the Monad
class -- to capture the similarities betweendifferent types.
这篇关于除Monad之外,还有哪些其他方式可以用纯粹的功能语言来处理?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!