本节的内容的有以下几点:
一、编程范式以及为什么要使用函数式编程?
二、什么是函数式编程
三、函数式编程的特征
一、编程范式以及为什么要使用函数式编程?
1、编程范式
我想大家应该在平时工作过程中,也许会因为项目而去另外学习或适应一种自己之前完全不熟悉的编程语言,对有着一定编程经验并已经熟练掌握一门语言的人来说,快速上手一门语言并应用于项目中也许并不是一件很困难的事情。但是情况并非总是如此,跨语言对一个程序员来说影响也许不是最大的,但是编程范式的变更也许会让一个程序员好一会都缓不过神来。
编程范式与编程语言不同,它很深层更内在,它是编程思想的凝练,通过编程语言的体现出来,又通过实践内化为程序员的一种编程思维。它并不容易在短时间内融汇贯通,而需要通过大量地实践加深对这种编程方式的理解。
我们常见的主流编程思维有三种:
1、逻辑式编程
2、命令式编程
3、函数式编程
三种编程范式都体现了各自独特的对用程序解决问题的思考。
1、逻辑式编程不注重解决问题的步骤,而是注重逻辑。它设定答案须符合的规则来解决问题,而非设定步骤来解决问题:规则+事实=结果。利用它编写的程序不是由指令序列组成,而是由一系列公理或定义对象之间关系的规则组。
2、命令式编程关心解决问题的步骤。它需要我们制定好对应解决某问题的一系列步骤,且让程序严格按照步骤去执行。它编写的程序需要我们去考虑在编码范围内需要考虑的一切问题,包括性能,边界验证,资源回收等。
3、函数式编程关心的是数据的映射,它重视更高层面上数据集之间的变换关系,而不是编制程序执行的每一步。它在机器学习算法高度发展的今天,变成了一种算法实现的主要编程范式之一。它的思维方式是将数据集的变换和数据上计算逻辑组合起来产生结果。编码者不需要过多关心数据集中每个元素的具体变换步骤,只需要在数据集合上组织计算逻辑并触发计算。
二、什么是函数式编程
正如上面提到的,函数式编程是一种面向数据映射的编程范式,它的目标是使用纯净的函数来表达问题的解决方式。
所谓函数式编程的函数本质,并不是指我们编程语言中的函数(例如python的def之类的),而是数学中的函数映射,这种映射只是接受参数,并得到一个结果,它并不会对外界产生任何影响,这样的一个函数的好处非常多,它们更有利于模块化,因此更容易测试、复用、并行化、泛化以及推导。
我们可以把函数对外界产生的影响称之为副作用,下面一个例子来说明,带有副作用的函数是如何造成困扰的。
一个简单的副作用例子
我们为玩具店购买玩具来编写一段程序,程序目的是购买一个玩具,并在信用卡上扣费
class Shop{
def buyToy(cc: CreditCart): Toy = {
val toy = Toy()
cc.charge(toy.price) // 副作用的源头
toy
}
}
class CreditCart{
// deduct
def charge(price: Double) = ???
}
case class Toy(val price: Double = 10)
cc.charge(toy.price)就是副作用的源头,因为信用卡的计费可能会涉及到外部世界的一系列交互,我们的函数只不过想要返回一个玩具,而其它额外的行为也随之发生了,这就是副作用。
这样的副作用导致很难进行测试,因为我们不希望我们的测试方法真的去走一遍信用卡和外部交互的流程。这种对可测试性的修改就意味着设计的修改:按理说CreditCard不应该知道如何去跟信用卡公司去进行实际扣费和持久化计费到内部系统中,我们可以让CreditCard忽略这件事,通过一个Payments接口,与外部交互的逻辑都托管给这个实现这个Payments的对象,然后分别实现一个为真正执行计费逻辑的Payments和一个用于测试的MockPayments。这样的做法使得模块更加模块化和可测试。
class Shop{
def buyToy(cc: CreditCart, p: Payments): Toy = {
val toy = Toy()
p.charge(cc, toy.price)
toy
}
}
trait Payments{
def charge(cc: CreditCart, price: Double)
}
我们这里再考虑一个问题:buyToy方法很难复用!例如一个客户想要购买20个玩具,最理想的是复用这个方法,调用20次进行扣费,不管是从实际意义上的手续费角度,还是从支付系统的调用的性能方面都有十分不理的影响。当然,我们还可以使用一个新的方法buyToys去实现,那么重复的代码逻辑会很多,而且会失去代码复用性和组合性。
去除副作用
函数式的解决方案就是去除副作用,我们可以不需要在买玩具的时候把扣费的逻辑执行了,可以把这个费用本身和玩具一起返回,我们再来改造一下代码:
class Shop{
def buyToy(cc: CreditCart): (Toy, Charge) = {
val toy = Toy()
(toy, Charge(cc, toy.price))
}
}
case class Charge(cc: CreditCart, amount: Double){
def combine(other: Charge): Charge = {
if(cc == other.cc)
Charge(cc, amount + other.amount)
else
throw new RuntimeException("不允许不同信用卡扣费")
}
}
在这段执行逻辑中,我们并没有在buyToy的方法中进行任何结算费用的操作,而只是返回物品本身和它的费用,我们希望的是把副作用剥离到更外层,而不是在函数调用的过程中进行,那么我们的结算多个Toy的动作也就更好完成了。
def buyToys(cc: CreditCart, n: Int): (List[Toy], Charge) = {
val purchases : List[(Toy, Charge)] = List.fill(n)(buyToy(cc))
// List[(A, B)] => (List[A], List[B])
val (toys, charges) = purchases.unzip
(toys, charges.reduceLeft(_.combine(_))) // 合并消费
}
现在我们可以把购买玩具的逻辑和付账逻辑隔离开,并可以复用代码实现多个玩具的购买。
相比之前使用Payments接口而言,我们使用Charge作为一等值的来隔离副作用,将
购买->付账(副作用)->得到玩具
的逻辑转变为
(购买->得到账单(可合并)->得到玩具)*->账单一并结账(副作用)
我们可以自己实现一个Payments对象在最后结算Charge里的price,但是Toy类并不需要了解它。
买玩具小结
我们在这个例子中看到如何把计费的创建过程与实际的处理过程进行分离。总的来说,就是把这些副作用推到程序的外层,来转化任何带有副作用的函数。对于优秀的函数式编程者来说,程序的实现就是一层纯的内核和一层很薄的外围来处理副作用。
三、函数式编程的特征
纯函数
我们在前面提到过纯函数的这一概念,这里给出它的精确定义:如果一个函数在程序执行的过程中出了根据输入参数给出结果之外,对外界没有任何其它的影响,那么可以说这一类函数是没有副作用的,这类函数也称为纯函数。
例如Scala中1 + 2(+实际上是一个中置操作符,可以被改写为1.+(2)),那么函数+只接受2为参数,然后与1相加返回一个新的整型3,整个过程没有引入到除了参数和调用者外的任意一个外界变化。
引用透明和替代模型
纯函数为函数式编程带来的一个好处就是:纯函数更容易推理,这就使得我们程序执行的推导过程更为流畅和自然。我们需要走到更高的层次去看看这些好处是怎么来的。
(为了叙述下面的内容,我们先来说明一下编码层次:函数<表达式<程序。)
我们上升至表达式的领域来:对于1 + 2这个表达式,它在任何一个地方都可以被它的结果3直接取代而不会引起程序的任何变更,我们称之为引用透明(表达式层面上的)。当调用一个函数时传入的表达式是引用透明的,并且函数的调用也是引用透明的,那么这个函数就是一个纯函数。纯函数要求无论进行来任何操作都可以用它的返回值来代替它,这种限制使得程序的求值可以通过简单自然的推导得出,我们称之为替代模型(程序层面上的)。如果程序中每个表达式都是引用透明的,那么我们可以使用替代模型来进行等式推理,就例如我们的代数方程一般。
替代模型的之所以很容易进行推理,因为它对运算的影响是局部的,只需要理解局部的计算逻辑,不需要在每一个表达式执行过程中都纵观全局的变化,对于程序的执行可如对代数推理一般流畅而自然地进行。它使得程序进行模块化变得十分简单而清晰,而模块化的函数更容易被测试和进一步的组合,提供程序的整体质量。
小结
总的来说,函数式编程的相对于其它编程范式来说有着它独特的优势,尤其是对于我熟知的命令式范式来说,它展现了一种完全不同的编程思维。在本章笔者也有一些对问题的思考:
去除了副作用之后,所有问题的都有一套函数式的编程方案嘛?
笔者认为,Scala在意的是,如何进行函数式编程,并非所有问题的最佳方案都是使用函数式编程范式解决,函数式范式有自己的适用场景。
引用:
https://www.zhihu.com/question/28292740
《Scala函数式编程》
网友评论