Haskell 当中是怎样处理多个 IO 请求的控制流的?
在 Node,js 当中, 使用在业务逻辑里可能需要控制大量 IO 操作,
于是出现了 async when 这样专门控制异步操作的库,
以及出现了 Promise 规范, 看网上的介绍, Promise 和 Monad 有不小的关联
http://www.ituring.com.cn/article/50561
那么, 我想知道, Haskell 当中是怎样用 Monad 控制复杂的 IO 操作的?
比如这样一些例子:
- 读取多个文件, 合并显示结果
- 读取多个文件, 过滤掉空的文件, 返回结果
- 读取多个文件, 兼容部分操作报错
- 读取多个文件, 返回最先读取的两个
- 读取多个文件, 返回限定时间内完成的几个
- 读取多个文件, 再按文件当中固定固定语法读取相关的文件
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
实际上 Haskell 是把所有相关东西都扔到了
IO
里面,没有特别精细地控制(STM
是个特例)。Haskell 的线程模型是类似 Go 的用户态轻量级线程,通过
forkIO
创建一个新的执行流。对新线程来说,它本身是个IO ()
,对外面的父线程来说,这个forkIO
也仅仅是产生一个IO ThreadId
。于是诞生了一个问题,这两个线程之前是没有同步和通信的。于是 GHC 又提供了MVar
,TVar
等原语来同步线程、发送数据。其他的库基本都是基于这套
IO
monad 上的原语实现的。这套原语足够灵活也足够熟悉,所以大部分情况下写并发代码和在其他语言里写出的代码差不多,尤其是 Go。tl;dr: Concurrent Haskell 本身提供的是一个较低级的、只有
IO
的库。一个常见的 pattern 是我们
forkIO
出子任务后需要等待这个结果反馈给父进程。于是可以考虑封装这个 pattern:上面这段代码非常直观简单,不过实际工程要考虑异步异常等细节,这里暂且忽略。从用户的角度观察一下,不难发现
Async a
代表一个 “未来某个时刻可能获知的a
类型的值”。下面有两种角度可以去进一步研究
Async a
。Async a
构造出更复杂的计算。Async
的代数性质。这两种视角其实是一致的,这里介绍一下多数人不太熟悉的第二种。(所谓的代数,就是找到一些算符和一些等式,类似于 a(b+c)=ab+a*c 这样的。)
假设我们有两个
async
,可以做什么?理应有这些操作:我们发现这非常类似于
Bool
的and
和or
。受到它们的启发,我们可以定义在 Async 上定义半群了!如果引入immediate :: a -> Async a
和never :: Async a
,我们还可以把半群扩展成幺半群。这与加法、乘法完全一致,我们甚至还有分配律:不过,只有这些操作还不够,有很多操作是无法表达的。比如说,“顺序”:先取得 Async a 的 a,再用这个值计算后得到 Async b。
观察这个类型,我相信你已经看出来这就是 Monad 的 bind 操作了 :-) 你可能会问:Async 是不是还缺一个 return?其实我们已经在上面定义了
immediate
,它 exactly 就是 return。那么 Async 是不是 monad 呢?我们还需要验证 3 条 monad laws:andThen (immediate a) h === h a
andThen m immediate === m
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 一下才会开始计算。简单来说就是Haskell默认是lazy的, 所以IO就有比较大的问题参见http://stackoverflow.com/q/5892653
后来Oleg出了iteratee
大概就是一个数据流的抽象, 然后数据分为一块一块, 每一块都是一个状态机, 自己管理自己的生命周期(资源管理)
以及后来衍生出的pipes, Conduit, machines, 都是在这个抽象上的不同的trade off
处理多个文件的话, 就是处理这些流的composability问题
p.s LZ还认得我吗?