目前而言,相比深奥琐碎的教程,感觉做编程练习提高更大。
以前我轻视了像LeetCode上面的编程练习,觉得实用性低(不妨碍个别解答难度很大)或者问题背景太简单(输入的不是数组就是字符串),结果做了一些特别是用两种编程语言比较着做,发现这些练习意义很大。
一种语言是Java,另一种是Racket(可视为Lisp的方言)。
Java做题发现哪怕是标准解法也实现不了教科书般的“优雅”、“抽象化”,你要考虑控制结构的种种细节,比如for循环中发生的一些边缘情况,可读性、运算效率、简单方法优先这些只能互相平衡。或许实际工作中遇到的问题也是如此,以前我一度误解了代码注释,认为注释超过一行则说明代码本身可读性差、缺少对问题的精辟分析(我的代码就经常大量注释),这些情况用一个词概括就是“琐碎”。
在使用中Racket我避免非常命令式的方法,比如先创建一个值然后在过程中改变它(参考遍历数组找到最大值的过程),但函数式编程很难绕开难点实现与命令式解决同样问题的效果。更加意识到编程范式的适用性与具体问题关系密切。
数据结构是计算的开始,算法是基于数据结构的,数据结构反过来要服务于算法。目前我观察到,函数式编程很难使用栈、队、列、堆甚至树这些数据结构,就是用了也不顺手,经过思考我意识到原因在于指令式和函数式之间的根本区别:外部效应。
函数式编程所谓的不改变状态严谨地说是不改变外部状态,一个函数输入一个值输出一个值,所有内部运算过程对外部其他东西没有影响。至于函数式编程其他方面的东西,我认为都是围绕这个特征来的。
像栈、队、列、堆、树这些数据结构我们可以用本身“不可变”的成分(如Lisp族中的list)来表示它们,甚至实现信息封装(参考SICP中实现数的方法),但一旦遇到这种情况就不好办了:多个过程需要利用同一对象进行操作。
举个简单例子就是LeetCode第一题,指令式编程的方法遍历两次很简单,一目了然,而这个嵌套遍历的过程用Racket等函数式语言是可以实现的,方法是把公共变量(数组/list元素的指针/引用)作为参数在修改后不断传递,但目前为止我还没找到用Racket自带库并且简单易懂的方法。Racket相比Common Lisp也升级了不少,但仍然很难。
这个讨论就说明了这个问题。
栈、队、列、堆、树这些数据结构在函数式编程中不好用的原因是,一旦遇到多个过程(刚才的例子还是嵌套的),如果不接受外部效应那不同过程之间协调的成本非常高。
结论:都是为了解决问题,函数式编程更适合高层次抽象,解藕程度高的个部分实现细节不必纠结编程范式。