Haskell 当中是怎样处理多个 IO 请求的控制流的?

题叶
  • 17.3k

在 Node,js 当中, 使用在业务逻辑里可能需要控制大量 IO 操作,
于是出现了 async when 这样专门控制异步操作的库,
以及出现了 Promise 规范, 看网上的介绍, Promise 和 Monad 有不小的关联
http://www.ituring.com.cn/article/50561

那么, 我想知道, Haskell 当中是怎样用 Monad 控制复杂的 IO 操作的?
比如这样一些例子:

  • 读取多个文件, 合并显示结果
  • 读取多个文件, 过滤掉空的文件, 返回结果
  • 读取多个文件, 兼容部分操作报错
  • 读取多个文件, 返回最先读取的两个
  • 读取多个文件, 返回限定时间内完成的几个
  • 读取多个文件, 再按文件当中固定固定语法读取相关的文件
回复
阅读 5k
2 个回答
Haskell 当中是怎样用 Monad 控制复杂的 IO 操作的

实际上 Haskell 是把所有相关东西都扔到了 IO 里面,没有特别精细地控制(STM 是个特例)。

Haskell 的线程模型是类似 Go 的用户态轻量级线程,通过 forkIO 创建一个新的执行流。对新线程来说,它本身是个 IO (),对外面的父线程来说,这个 forkIO 也仅仅是产生一个 IO ThreadId。于是诞生了一个问题,这两个线程之前是没有同步和通信的。于是 GHC 又提供了 MVar, TVar 等原语来同步线程、发送数据。

其他的库基本都是基于这套 IO monad 上的原语实现的。这套原语足够灵活也足够熟悉,所以大部分情况下写并发代码和在其他语言里写出的代码差不多,尤其是 Go

tl;dr: Concurrent Haskell 本身提供的是一个较低级的、只有 IO 的库。


Promise 和 Monad 有不小的关联

一个常见的 pattern 是我们 forkIO 出子任务后需要等待这个结果反馈给父进程。于是可以考虑封装这个 pattern:

data Async a = Async (MVar a)

-- 需要一个 “构造”出  Async 的东西
async :: IO a -> IO (Async a)
async job = do
  mvar   <- newEmptyMVar
  handle <- forkIO $ do
    result <- job
    putMVar mvar result
  return $ Async mvar

-- 需要一个 “脱掉” Async 的东西
wait :: Async a -> IO a
wait (Async mvar) = takeMVar mvar

上面这段代码非常直观简单,不过实际工程要考虑异步异常等细节,这里暂且忽略。从用户的角度观察一下,不难发现 Async a 代表一个 “未来某个时刻可能获知的 a 类型的值”。

下面有两种角度可以去进一步研究 Async a

  1. 从计算的角度,我们可以如何用 Async a 构造出更复杂的计算。
  2. 从代数的角度,研究 Async 的代数性质。

这两种视角其实是一致的,这里介绍一下多数人不太熟悉的第二种。(所谓的代数,就是找到一些算符和一些等式,类似于 a(b+c)=ab+a*c 这样的。)

假设我们有两个 async,可以做什么?理应有这些操作:

-- 组合两个小任务变成一个大任务。两个小任务都完成时才认为大任务完成。
both :: Async a -> Async b -> Async (a,b)

-- 组合两个小任务变成一个大任务。两个小任务其一完成时就认为大任务完成。
either :: Async a -> Async a -> Async a

我们发现这非常类似于 Boolandor。受到它们的启发,我们可以定义在 Async 上定义半群了!如果引入 immediate :: a -> Async anever :: Async a,我们还可以把半群扩展成幺半群。这与加法、乘法完全一致,我们甚至还有分配律:

a `both` (b `either` c) = (a `both` b) `either` (a `both` c)

不过,只有这些操作还不够,有很多操作是无法表达的。比如说,“顺序”:先取得 Async a 的 a,再用这个值计算后得到 Async b。

andThen :: Async a -> (b -> Async b) -> Async b

观察这个类型,我相信你已经看出来这就是 Monad 的 bind 操作了 :-) 你可能会问:Async 是不是还缺一个 return?其实我们已经在上面定义了 immediate,它 exactly 就是 return。那么 Async 是不是 monad 呢?我们还需要验证 3 条 monad laws:

  1. andThen (immediate a) h === h a
  2. andThen m immediate === m
  3. andThen (andThen m g) h === andThen m (\x -> andThen (g x) h)

运用我们对 Async 的直觉,这三条 law 一定是成立的。注意,到现在为止我们还没写一行代码!如果上面这三条 monad laws 不成立才是咄咄怪事。

到此为止,我们推导出了涵义是 “表示未来某个时刻才能拿到的值” 的类型构造器 Async :: * -> * 一定是 Functor, Applicative, Monad。尽管上面的推导是基于所谓的 “有栈协程” 的,但如果可以想到 “Monad 是描述计算的”,就会意识到 “无栈协程” 也可以用这种方式描述,只要满足我们的大前提:“表示未来某个时刻才能拿到的值”。

现在把上面的理论套到 JavaScript 上,Promise 就是上面的 Async(确切地说应该是 Async (Either Error a),并且 a 是被擦没了),then 就是 andThen。和 Monad 的关系已然呼之欲出了。

PS:我们还可以把 C#、Rust 等的 async 函数(事实上是无栈协程)放到同一个框架下考虑,如 Rust 的 Future<T> 完全与上面的 Async 一致。不过它们使用 delimited continuation 的角度去看更方便。

PSS:我们对 Async 到底何时执行没有进行任何约束。从用户角度来看,只能保证如果 wait 返回,那么结果一定已经到手了。所以上面的解读与常见语言、框架在执行策略上的不同没有矛盾。有栈协程语言(如 Haskell)常常是在 Async a 构造开始时就立刻进行计算,而无栈协程语言(如 Rust)可能需要 poll 一下才会开始计算。

c982695
  • 1
新手上路,请多包涵

简单来说就是Haskell默认是lazy的, 所以IO就有比较大的问题参见http://stackoverflow.com/q/5892653
后来Oleg出了iteratee
大概就是一个数据流的抽象, 然后数据分为一块一块, 每一块都是一个状态机, 自己管理自己的生命周期(资源管理)
以及后来衍生出的pipes, Conduit, machines, 都是在这个抽象上的不同的trade off
处理多个文件的话, 就是处理这些流的composability问题

p.s LZ还认得我吗?

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
宣传栏