王垠:编程的宗派( 四 )


副作用的根本价值对数据结构的忽视 , 跟纯函数式语言盲目排斥副作用的“教义”有很大关系 。 过度的使用副作用当然是有害的 , 然而副作用这种东西 , 其实是根本的 , 有用的 。 对于这一点 , 我喜欢跟人这样讲:在计算机和电子线路最开头发明的时候 , 所有的线路都是“纯”的 , 因为逻辑门和导线没有任何记忆数据的能力 。 后来有人发明了触发器(flip-flop) , 才有了所谓“副作用” 。 是副作用让我们可以存储中间数据 , 从而不需要把所有数据都通过不同的导线传输到需要的地方 。 没有副作用的语言 , 就像一个没有无线电 , 没有光的世界 , 所有的数据都必须通过实在的导线传递 , 这许多纷繁的电缆 , 必须被正确的连接和组织 , 才能达到需要的效果 。 我们为什么喜欢WiFi , 4G网 , Bluetooth , 这也就是为什么一个语言不应该是“纯”的 。
副作用也是某些重要的数据结构的重要组成元素 。 其中一个例子是哈希表 。 纯函数语言的拥护者喜欢盲目的排斥哈希表的价值 , 说自己可以用纯的树结构来达到一样的效果 。 然而事实却是 , 这些纯的数据结构是不可能达到有副作用的数据结构的性能的 。 所谓纯函数数据结构 , 因为在每一次“修改”时都需要保留旧的结构 , 所以往往需要大量的拷贝数据 , 然后依赖垃圾回收(GC)去消灭这些旧的数据 。 要知道 , 内存的分配和释放都是需要时间和能量的 。 盲目的依赖GC , 导致了纯函数数据结构内存分配和释放过于频繁 , 无法达到有副作用数据结构的性能 。 要知道 , 副作用是电子线路和物理支持的高级功能 。 盲目的相信和使用纯函数写法 , 其实是在浪费已有的物理支持的操作 。
fold以及其他大量使用fold和currying的代码 , 写起来貌似很酷 , 读起来却不必要的痛苦 。 很多人根本不明白fold的本质 , 却老喜欢用它 , 因为他们觉得那是函数式编程的“精华” , 可以显示自己的聪明 。 然而他们没有看到的是 , 其实fold包含的 , 只不过是在列表(list)上做递归的“通用模板” , 这个模板需要你填进去三个参数 , 就可以生成一个新的递归函数调用 。 所以每一个fold的调用 , 本质上都包含了一个在列表上的递归函数定义 。 fold的问题在于 , 它定义了一个递归函数 , 却没有给它一个一目了然的名字 。 使用fold的结果是 , 每次看到一个fold调用 , 你都需要重新读懂它的定义 , 琢磨它到底是干什么的 。 而且fold调用只显示了递归模板需要的部分 , 而把递归的主体隐藏在了fold本身的“框架”里 。 比起直接写出整个递归定义 , 这种遮遮掩掩的做法 , 其实是更难理解的 。 比如 , 当你看到这句Haskell代码:
foldr (+) 0 [1,2,3]你知道它是做什么的吗?也许你一秒钟之后就凭经验琢磨出 , 它是在对[1,2,3]里的数字进行求和 , 本质上相当于sum [1,2,3] 。 虽然只花了一秒钟 , 可你仍然需要琢磨 。 如果fold里面带有更复杂的函数 , 而不是+ , 那么你可能一分钟都琢磨不透 。 写起来倒没有费很大力气 , 可为什么我每次读这段代码 , 都需要看到+和0这两个跟自己的意图毫无关系的东西?万一有人不小心写错了 , 那里其实不是+和0怎么办?为什么我需要搞清楚+, 0, [1,2,3]的相对位置以及它们的含义?这样的写法其实还不如老老实实写一个递归函数 , 给它一个有意义名字(比如sum) , 这样以后看到这个名字被调用 , 比如sum [1,2,3] , 你想都不用想就知道它要干什么 。 定义sum这样的名字虽然稍微增加了写代码时的工作 , 却给读代码的时候带来了方便 。 为了写的时候简洁或者很酷而用fold , 其实增加了读代码时的脑力开销 。 要知道代码被读的次数 , 要比被写的次数多很多 , 所以使用fold往往是得不偿失的 。 然而 , 被函数式编程洗脑的人 , 却看不到这一点 。 他们太在乎显示给别人看 , 我也会用fold!