从 Stream 和函数式编程想到的

f627 9年前

面向对象的问题

我作为自学的程序员, 绕了很多弯子, 缺了很多编程的基础理论
作为前端我也能拉很多人下水, 因为很多写界面的人也是自学的
编程语言从 Fortran 跟 Lisp 已经被研究了半个世纪, 理论成果也是连篇累牍
我们常常觉得自己已经在编程了, 但是基于什么编程呢?

首先编程当然是对真实世界的情况的模拟, 这一点作为基础
当然问题关键是, 用什么来模拟?
一类是表达式, 或者说递归嵌套的表达式, 比如((x * x) + (y * y))
不过表达式功能有限, 注意到吗, 这里边是没有状态的
那么, 有状态的对象怎么模拟? 面向对象编程(OOP)说, 用面向对象啊

其实"对象(Object)"这个词有点滥用翻译了, 什么都是 Object 啊
OOP 当中的对象非常特别, 特指有内部状态的对象
比如鼠标的位置P, 通过P.get()读取, 会根据具体情况改变
面向对象以此为基础, 说, 这可以模拟真实世界, 对象有内部状态
于是编程就是各种对象通过接口交换内部状态, 而最终形成

然而函数式编程来说, 它也有对象, 但并没有可变的那种对象
Rich Hichkey 说, 一个值变来变去, 那就是不可靠的
特别是在复杂的系统中, 甚至并行当中, 发生混乱了怎么定位
其实函数式编程也有讲上边的问题, 真实世界各种状态, 怎么模拟?
给了个例子, 比如英国国王是谁? 不清楚! 但是具体哪一年英国国王是谁? 知道了!
值并不是任意改变的, 而是随着时间改变的, 就像f(x)函数
内存里的数据也不是任意改变的, 而是每个时刻有一个确定的状态

SICP

所以, 在函数式变成当中, 随着时间改变的就不是变量, 而是"流(Stream)"
这一点, 在 SICP 当中关于流的介绍做了这样的说明:
http://sarabander.github.io/sicp/html/3_002e5.xhtml#g_t3_002e5

Can we avoid identifying time in the computer with time in the modeled world? Must we make the model change with time in order to model phenomena in a changing world?

Think about the issue in terms of mathematical functions. We can describe the time-varying behavior of a quantity x as a function of time x(t). If we concentrate on x instant by instant, we think of it as a changing quantity. Yet if we concentrate on the entire time history of values, we do not emphasize change — the function itself does not change.

If time is measured in discrete steps, then we can model a time function as a (possibly infinite) sequence. Stream processing lets us model systems that have state without ever using assignment or mutable data.

刚接触 Lisp 那时经常看到帖子里有人推荐 SICP, 说是很棒的书
里边有大量的习题, 我看不下去, 到最近也只是把文字描述浏览一遍
http://kidneyball.iteye.com/blog/922953 说的彻头彻尾的教程啊

SICP,Structure and Interpretation of Computer Programs,计算机程序的构造和解释,是美国麻省理工学院(MIT)的计算机科学(CS)与电子工程(EE)本科的一门必修课。这本书在1984年 出版,而自从1980年开始,20多年来此书的内容一直是MIT的计算机编程入门课程,并且被世界各地百余所大学效仿。SICP是基于LISP语言展开论 述的,直到2008年,才被另一门基于python语言,但原理相同的课程取代。

不管怎样, 真的是一本很棒的书, 即便我很不喜欢看, 也不爱做习题
现在的编程当中遇到的很多问题, 至少理论上很早就被研究过了
SICP 当中程序数据甚至语言本身的抽象, 今天的脚本语言仍然混淆
而我也最近才慢慢学习理解到 Stream 这个概念有多么重要

架构图

先抛开代码, 看看现实当中的大问题我们怎样思考, 我们常常画图
图形界面怎样编写, 有 MVC 这样的方案, 拆分出一些大的模块
然后用户有操作, 服务器有数据, 就开始在几个模块之间流动, 像这样:

React 造出来 Flux, 至少看架构图差别不是那么大, 也是几个组成部分
然后用户操作 Action, 以及数据, 沿着一个方向流动, 像这样:

Apple 的 JavaScript 引擎, 用 LLVM 优化编译 JavaScript 代码
整个流程也划分成了一些模块, 然后数据在模块之间逐个传递, 像这样:

对于前端开发者来说 DOM 和 CSSOM 的解析也是被拆解的过程
两者分别解析, 最后合并成一个用于渲染的 DOM Tree, 最后渲染
整个过程其实是从网络加载代码, 沿着箭头流动处理的过程, 像这样:

游戏引擎的架构我不熟悉, 不过拆分了模块之后大概的意思也好懂, 像这样:

我想说的是, 解决问题时, 拆分模块, 在之间传递数据, 很通用的办法
编程常常就是大问题拆成小问题, 然后拼在一起解决, 也是这意思

FBP(Flow-based Programming)

如果按照上边的思路直接去找类似的编程语言, 那也是有的
FBP 的概念在 1970s 就出来了, 具体说来我也不懂, 自己看 Wiki 吧
https://en.wikipedia.org/wiki/Flow-based_programming
不过前两年 Node 社区有个 Noflo 很火, 看界面也是挺棒的
只要找到需要的模块, 然后连一连线条, 就能把程序写出来了:

Noflo 也许不实用, 但是基于 Quartz Composer 的 Origami 总能用了
这个软件里通过拖拽就能创建出可以交互的图形界面
第一次看同事演示的时候, 尽管知道 Noflo, 还是觉得这很高明很强大
而且也说明, 复杂程序分明是可以不写代码, 直接拖着就出来了的:

然后来回顾一下, 我们每天写的代码, A 状态改变, B 怎么跟着改变?
A 发生了什么, 我做事件监听, 然后对 B 进行操作, B 终于跟着改变了
可是用上边的图形呢, 拖一条线, 把 B 跟 A 关联起来, 不就好了吗

Streams 和 Monad

晚上我翻到一片文章, 讲的是 Swift 当中的 Monad
我知道这文章就是从 Haskell 改写的, 而且图片还非常好玩
http://www.mokacoding.com/blog/functor-applicative-monads-in-pictures/



Stream 在 Haskell 里大概也是 Monad 实现的, 细节我有点模糊
反正 Haskell 里的 State 都用 Haskell 封装隔离, 也不会差太多了
Monad 很有意思, 被比作一个盒子, 盒子里装了数据, 甚至还装了函数
然后 Monad 讲的都是数据甚至函数包在盒子里怎么去操作的故事
过程不难懂, 但有个事情很费解, 好端端地干嘛把数据装进盒子里?
我写 CoffeeScript 那么久了, 直接操作数据多方便, 为什么要盒子包起来?

到这里! 我要跳跃了, 回想一下前面说的 Stream, 还有架构图, 数据在哪?
Stream 是随着时间改变的数据, 是一个流, 不是单个单个的数据, 有盒子对吧
架构图上, 数据在模块里被处理, 沿着箭头被模块发送到另一个模块,
然而, 注意编程语言怎样发送数据,O.emit(data)吗, 但是内部实现是什么?
再想一下, 是应该有一个管道, 数据被发送到管道里去了, 也像是盒子对吧
模拟状态的时候, 常常有一层封装, 就像是盒子, 或者说管道, 比如说 Stream

而且如果把 Stream 架构图上的箭头用 Stream 来替换的话, 就更清晰了
通常调用函数获取数据, 在图上相当于从后边调用前边模块的数据
这个过程看起来流程后面的组件在驱动, 而不是前面的模块在驱动
但是 Stream 的话, 当然是前面的模块执行完, 再流动到后面的模块
加入流的概念之后, 架构图的方向和思路就清晰多了
我这样说, 是因为看过 Elm 和 PureScript 当中 Signal 实现 Flux 的例子
相比 JavaScript 当中 Flux 用 Dispatcher, Signal 的写法清晰太多了

我们想要模拟现实世界, 然而面向对象声称的办法依然不够强大
可变的状态, 状态一改变就消失了, 再也找不回来
监听和操作数据, 写起代码来却是各种繁琐, 依然需要继续做抽象
我想到 Node.js 的 Stream, JavaScript 是 OOP, 然而实现了 Stream
Stream 可以是基于 EventEmitter 的封装, 把数据包在事件流当中的
而 Stream 带来什么样的方便呢, 组合起来很很灵活

流还是很难

前面罗列那么多, 并不是说我理解了, 而是说终于我把一些困惑串在一起了
我知道了为什么会有 Stream, 为什么 Stream 很重要, 我应该往哪儿学习
Node.js 当中实现的流相对简单, 前端事件流也还不错
但是也有复杂的, 敢不敢看看 PureScript 当中 UI 操作的 Signal 是如何处理的
http://begriffs.com/posts/2015-07-10-design-of-purescript-halogen.html
Signal 相当于流的概念, 但在 ADT 类型当中真是太难理解了

另外 Go 的 Channel 似乎也是流, 虽然有了新的名字, 新的操作语法
还有 Java 之类我无法理解的语言, 其中当然也实现了 Stream, 但我不懂
不懂所以文章写到这里就结尾吧...


来自:http://segmentfault.com/a/1190000002992542