从命令式编程,到面向对象风靡,再到现在一个古老的编程范式-函数式编程的出现…
编程范式
范式 - 科学哲学概念,简单来说就是基本纲领,范式之间不可通约,范式转换是断裂性革命性转换
- 命令式编程: 专注于”如何去做”,这样不管”做什么”,解决某一问题的具体算法实现
- 面向过程
就是分析解决问题所需要的步骤,然后把这些步骤一步一步实现 - 面向对象
所谓面向对象,就是将程序里面的模型看做一个一个的对象。对象和对象之间会产生彼此的联系。使用对象来解决问题
- 面向过程
- 声明式编程: 非命令式的编程都可归为声明式编程,专注于”做什么”而不是”如何去做”
- 函数式编程
- 逻辑式编程
lambda
函数编程语言最重要的基础是λ演算(lambda calculus),而且λ演算的函数可以接受函数当作输入(参数)和输出(返回值)
在lambda 演算中,每个表达式都代表一个只有单独参数的函数,这个函数的参数本身也是一个只有单一参数的函数,同时,函数的值又是一个只有单一参数的函数。函数是通过 lambda 表达式匿名地定义的,这个表达式说明了此函数将对其参数进行什么操作。
函数式编程特点
- 函数是第一等公民
那么相当于它支持了高阶函数,高阶函数就是至少满足下列一个条件 1.接受一个或多个函数作为输入 2.输出一个函数 - 纯函数
函数的结果只依赖于输入的参数且与外部系统状态无关——只要输入相同,返回值总是不变的。
除了返回值外,不修改程序的外部状态(比如全局变量、入参)。——满足这个条件也被称作“没有副作用 (side effect)”
如果一个表达式,对于相同的输入,总是有相同的结果并且不修改程序其他部分的状态,那么这个表达式是引用透明的。
F(x) = ax +b
- 不变性
=: 不是指派,而是模式比對
把等號左邊的模式,跟右邊的值比對看看。如果有辦法找到讓等式成立的情況,那就幫你綁定變數。[a, a, b] = [1, 2, 3] #=> ** (MatchError) no match of right hand side value: [1,2,3]
a
不可能又是1,又是2[a, a, b] = [1, 1, 3] # => [1, 1, 3]
- 尾递归优化 如果递归很深的话,stack受不了,并会导致性能大幅度下降。尾递归优化技术——每次递归时都会重用stack,这样一来能够提升性能,当然,这需要语言或编译器的支持。Python就不支持
一等公民的纯函数带来了什么
- 可以利用Memoization技术提升性能
满足引用透明的表达式(包括任意纯函数调用)满足这样一个特点,就是任意两次调用只要输入相同,其结果总是不变的。于是可以将第一次的计算结果缓存起来,遇到下一次执行时直接替换,依然能保证程序的正确性。这种优化方法称为Memoization - 延迟求值 (Lazy Evaluation)
延迟求值是指表达式不在它被绑定到变量时就立即求值,而是在该值被用到的时候才计算求值
很显然,延迟求值的正确性需要纯函数的性质来保证——即在输入参数相同的情况下,无论什么时候被执行,结果总是不变的 - 可以使用 currying 技术进行函数封装
curryiny 将接受多个参数的函数变换成接受其中部分参数,并且返回接受余下参数的新函数。
比如幂函数pow(x, y),它接受两个参数——x和y,计算x^y。使用currying技术,可以将y固定为2转化为只接受单一参数x的平方函数,或者将y固定为3转化为立方函数 - parallelization并行
所谓并行的意思就是在并行环境下,各个线程之间不需要同步或互斥。
函数式和面向对象比较
面向对象核心是状态,函数式核心是数据 所以面向对象更适合对业务(复杂的状态变化)的设计,而函数式适合对功能(复杂的数据变化)的设计 随着面向对象设计方式的发展,理论是越来越完善,复杂度也越来越高,面向对象的设计方式很多时候不再把目光投向实际的问题, 而是追求所谓的设计技巧。 函数式编程则更加直接,将问题转化为对数据的处理,关注点更容易集中在问题本身
函数式和AI
函数式编程能够再度火起来,和AI也有一定的关系,机器学习本身就是对大量数据的学习和处理,通过数据来训练出算法。这种模式更加适合函数式编程,而面向对象面对这种未知结果的学习,抽象会非常困难。
java的函数式编程
java8提供了stream, optional 实现一个不可变的java类:
- 所有字段标记为final
- 类标记为final, 防止被子类覆盖
- 不要提供无参数构造器,至少提供一个构造器,除构造器外不提供任何改变状态的方法
参考资料
- 《functional thinking》
- 可能是最好的函数式编程入门