[Haskell] Monadic IO System

1. Pure Language

Haskell是一个纯函数式编程语言(pure language),
任何一个函数的调用结果只会取决于它的参数。
而C语言中的rand(),getchar()函数,每次调用都会返回不同的结果,
因此,在Haskell实现它们中几乎是不可能的。

此外,Haskell中的函数是没有副作用的(side effect),
调用它们,不会影响外部世界(real world)。
比如,不能修改文件,不能在屏幕上显示字符,不能通过网络发送消息,等等。

这意味着,任何相同参数的函数调用,都可以被它的调用结果所取代。
而且语言本身保证了,这样做不会影响程序的最终结果。


2. Compiler optimization

C编译器会猜测哪些函数没有副作用,不会依赖可变的全局变量。
可是,如果猜错了的话,这种优化就会不小心改变了程序的语义。
因此,C编译器总是保守的猜测,或者需要程序员指出某个函数是否纯函数。

与C编译器不同的是,Haskell编译器是一系列纯粹的数学变换,
因此,结果可能是高度优化的。
此外,纯粹的数学计算,可以很容易被切分成多线程并行执行,
正好符合当今计算机多核的发展趋势。
而且,纯粹的数学计算,错误会更少,也容易被验证,
它会使Haskell代码更健壮,执行效率更高。

Haskell的纯函数性,允许编译器只进行那些必要的调用,称之为惰性求值(lazy evaluation)。
对于那些纯数学计算,这是一件极好的事情,
可是对于IO操作(IO action),就出问题了。

putStrLn "Press any key to begin formatting"

以上函数调用,不会返回任何有意义的结果,
编译器可能会忽略,或者重新排列类似这种函数的调用,
我们如何避免它这么做呢?

即,在惰性求值的语言中,我们如何实现那些依赖状态改变的算法,
如何写出有副作用的程序。
这个问题在Haskell历史上出现过了很多种解决方案,
目前标准的方法是使用Monad。


3. Monad的本来面目

Monad是范畴论(category theory)中的一个概念,范畴论是数学的一个分支。
幸运的是,为了解决IO问题和副作用,我们不必学它。

3.1 纯函数的问题

假如我们要在Haskell中实现getchar,它可能是Char类型的。

getchar :: Char
getchar = ...

然后,我们根据getchar可以实现get2chars

get2chars :: [Char]
get2chars = [getchar, getchar]

我们发现get2chars是有问题的。
(1)因为Haskell编译器把所有函数看做纯函数,所以,get2charsgetchar的两次调用,只会被执行一次。
(2)即使执行了两次getchar,编译器也不能保证哪个getchar先被执行。

3.2 重复调用

为了解决第一个问题,我们可以引入一个假的参数(fake parameter),
每次使用不同的参数调用getchar

getchar :: Int -> Char
getchar = ...

get2chars :: [Char]
get2chars = [getchar 1, getchar 2]

然后,get2chars也会遇到只会被调用一次的问题,也应该附带假的参数。

get2chars :: Int -> [Char]
get2chars _ = [getchar 1, getchar 2]

3.3 数据依赖

现在我们需要给编译器一些线索,让它知道哪个函数先被调用。
然而,除了数据的依赖关系(data dependency)之外,
Haskell语言本身并没有提供任何表示执行顺序的方法。

我们如何人为添加一些数据依赖呢?
如何保证第一个getchar比第二个getchar先执行呢?
为了达到这个目的,我们需要让第一个getchar额外返回一个值,
并把这个值传给第二个getchar

getchar :: Int -> (Char, Int)
getchar = ...

get2chars :: Int -> [Char]
get2chars _ = [a, b]
    where (a, i) = getchar 1
          (b, _) = getchar i

这样我们就解决了第二个问题,
由于第二个getchar需要第一个getchar的返回值i
所以,它必须等第一个getchar执行完才能被调用。

3.4 无用参数

以上我们给get2chars增加了一个额外的参数,但是没有使用它,
Haskell编译器十分聪明,它能看到这一点,会进行相应的优化,
因此get2chars还是无法被重复调用。

为了解决这个问题,我们不妨把get2chars的参数,传给第一个getchar

get2chars :: Int -> [Char]
get2chars i0 = [a, b]
    where (a, i1) = getchar i0
          (b, i2) = getchar i1

3.5 传递下去

此外,由于get2chars也需要建立数据依赖,
所以,也必须多返回一个参数。

get2chars :: Int -> (String, Int)
get2chars i0 = ...

get4chars :: [Char]
get4chars :: (a++b)
    where (a, i1) = get2chars i0
          (b, i2) = get2chars i1

这个额外返回的参数应该是什么呢?
如果我们使用整数常量,聪明的Haskell编译器也会看到这一点,
我们不如使用最后一个getchar额外返回的值,作为get2chars额外的返回值。

get2chars :: Int -> (String, Int)
get2chars i0 = ([a, b], i2)
    where (a, i1) = getchar i0
          (b, i2) = getchar i1

到目前为止,我们已经实现了Haskell的整个IO系统。


4. RealWorld

以上我们给那些有副作用的函数,增加了额外的参数和额外的返回值,
为了让它们可以被重复调用,并且保证它们的执行顺序。

于是,main函数应该具有以下类型,

main :: RealWorld -> ((), RealWorld)

其中,我们用RealWorld这个假的类型(fake type)替代了上文中的Int
RealWorld类型的值就像一个接力棒,在函数的执行过程中进行传递。
main调用某个IO函数的时候,它就把RealWorld传给它作为参数,
此外,这个IO函数还额外返回一个RealWorld传递给下一个IO函数。

为了描述的清晰起见,我们定义一个类型别名(type synonym)。

type IO a = RealWorld -> (a, RealWorld)

因此,main的类型就是IO ()getChar的类型就是IO Char,等等。
我们来看看main调用两次getChar的例子。

getChar :: IO Char    -- RealWorld -> (Char, RealWorld)
getChar = ...

main :: IO ()    -- RealWorld -> ((), RealWorld)
main world0 = let (a, world1) = getChar world0
                  (b, world2) = getChar world1
              in ((), world2)

mainworld0传给第一个getChar
这个getChar返回一个新的world1,传递给第二个getChar
然后main返回第二个getChar返回的world2


参考

IO inside

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,064评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,606评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,011评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,550评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,465评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,919评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,428评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,075评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,208评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,185评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,191评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,914评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,482评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,585评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,825评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,194评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,703评论 2 339

推荐阅读更多精彩内容