FP (Functional programming,函数式编程)
函数式编程理念
函数式编程 使得代码的编写、阅读、测试和重用都更容易了。
纯函数函数式程序构建于纯函数之上。纯函数没有副作用,它不依赖于除参数以外的其他任何东西。它影响外部世界的唯一途径,就是它的返回值。
数学函数都是纯函数。除了返回结果以外,求解过程也不会“做”任何其他的事情。
程序的输出是不纯粹的。使用println时,把数据推送给了输出流,外部世界就因此发生了改变。println 会产生什么样的结果,还取决于函数之外的状态:标准输出流可能被重定向、被关闭或是遭到损坏。
纯函数和不可变数据之间的联系是何等密切。
如果mystery是一个纯函数,那么无论它做什么,data-1和data-2必须是不可变的!否则,数据的变化将导致函数对于相同的 input ,却有不同的返回值。只要一小块可变数据就会毁掉这一切,导致整个函数调用链都不再纯粹。所以,一旦许诺编写纯函数,最终,在程序的大部分地方使用的都是不可变数据。
持久性数据结构
不可变数据是 Clojure 通向FP和处理状态的关键所在。
站在FP的这一面,纯函数不能出现诸如更新可变对象状态这样的副作用。
站在状态处理这一面,Clojure的引用类型也需要借助不可变数据结构,来实现其并发保证。
美中不足的是性能。当所有的数据都不可变时,“更新”会转变为“创建一份原始数据的副本,再加上我的更改”。这样做的话,会迅速地把内存耗尽!
Clojure数据结构并没有采用这种“复制一切”的天真方式。相反,所有的Clojure数据结构都是持久的(persistent)。
持久性,意味着新旧版本之间能高效的共享结构,通过这种方式,持久性数据结构能很好地维持其自身的旧副本。通过列表,可以很容易地视觉化结构共享。
创建一个列表b,它比a多了一个元素。
b重用了所有a的结构,而不是建立它自己的私有副本
只要可能,所有的Clojure数据结构都回采用共享。
惰性和递归
函数对其自身进行调用时,无论是直接还是间接的,就会形成递归。
有了惰性,对表达式的求值会被推迟至实际需要时才真正进行。
对一个惰性表达式进行求值,被称作表达式的变现(realizing)。
在Clojure中,函数和表达式都不是惰性化的。
序列则一般来说都是惰性的。
惰性技术还意味着纯函数。非纯函数就无法充分发挥惰性技术的作用了。作为一名程序员,必须控制好调用非纯函数的时机,因为如果调用的时间不同了,则其行为很可能也会不同!
引用透明性
惰性依赖于这样一种能力:在任何时刻,都可以用函数的结果来取代对其的调用。
具备这种能力的函数被称为是引用透明(Referential transparency)的,是因为对该函数的调用可被替换,却不会影响程序的行为。
引用透明的函数益处。
● 快存(Memoization),自动缓存结果。
● 自动并行化,将函数移至另外一个处理器,甚至另外一台机器进行求值。
纯函数的定义就是引用透明的。
FP的优势
相关的信息都包含在函数参数当中,直接呈现在面前。不必担心全局范围、会话范围、应用范围或线程范围什么的。因为同样的原因,函数式代码也更容易阅读的多。
更容易阅读和编写的代码,自然也更容易测试。
随着项目变得越来越大,往往也需要付出很多的努力来建立适当的环境,以便运行测试。
对于函数式代码而言,这样的问题要少得多,那是因为不存在参数以外的所谓“相关环境”。
函数式代码还提升了重用性。
● 发现和理解一块有用的代码。
● 将可重用代码与其他代码组合到一起
非纯函数妨碍了封装,它们允许来自外部世界的渗入(悄无声息的),并改变了代码的行为。另一方面,纯函数才是真正的封装良好,而且可以组合。无论把它们放到系统中的任何位置,它们的表现总是非常一致。
6条规则
1.避免直接递归。
Java 虚拟机无法优化递归调用,Clojure 的递归程序会撑爆它们的栈空间。
2.当产生的是 标量 (scalar values),或者是体积小还数量固定的序列时,可以使用recur。Clojure会对显式的recur进行调用优化。
3.当产生个头大,或是大小可变的序列时,让它成为惰性的,而不要用递归。这样,调用者就只需要为他们实际需要的那一部分买单。
4.小心不要让一个惰性序列变现的太多,多的超出需要。
5.熟悉序列库。这样就总能写出完全用不着recur或者惰性API的代码了。
6.细分。把看似简单的问题也尽可能划分为更小的块。这样就能发现蕴藏于序列库中的解决方案。于是代码更通用,可重用性也会更好。
规则5和规则6尤为重要。