scala与函数式编程——什么是函数式编程

什么是函数式编程?

  函数式编程是一种面向函数和函数组合的编程方式。
  什么是函数?从数学的角度,函数即Function,是从集合A到集合B的一种映射关系。如果集合A中的每一个元素都对应到集合B中的某一个元素,那么这种映射关系就叫做函数。比如每个人都有一个名字,那么“人”这个集合中的每一个元素,都能对应到String集合中的一个字符串,因此“将人通过名字映射到字符串”是一个函数,它的签名可以是mapToName(Person):String,换个名称,就是getName(Person):String
  再举一个例子,每个人都有父亲,因此“人”能通过“父亲”这个关系映射到另一个“人”,函数签名是getFather(Person):Person,当集合A与B相等时,称为自函数即Endofunction。同时注意上述两个函数是全局静态的,不像方法需要挂靠在特定的“类”之下。
  若是函数有多个参数,那么可以将这些参数看成一个新的数据类型或一个N元组。如sum(a:Int, b:Int):Int,可以看成是将一个(Int,Int)的元组映射到Int的一个函数。除了这些有名称的函数外,还可以通过Lambda表达式产生匿名函数,如(i,j) => i+j就定义了一个匿名的sum函数。
  那么函数组合呢?是指给定两个函数,f1(a):A->B(从A集合映射到B集合),f2(b):B->C(从B集合映射到C集合),那么就能将两个函数组合起来得到一个新的函数f3(a)=f2(f1(a)),从A映射到C,也写作f3=f2°f1或f1 andThen f2。以上面的两个函数为例子,能组合出第三个函数“获得父亲的名字”:

def getFatherName(p: Person):String = getName(getFather(p)) //通过两个函数来实现新函数
val getFatherName:Person=>String = getName andThen getFather //通过直接组合两个函数产生新函数

函数式编程有哪些特性?

  个人认为函数式编程虽然有很多重要特性,但核心的特性主要是函数的组合与引用透明,后续其它的特性以及优点均由这两个特性衍生而来。衍生的这些特性以及优点我们留到下篇介绍。

核心1. 关键在于函数的组合

  上面的getFatherName的函数是通过组合两个固定的函数产生的,如若将getFather替换为其它任何从“人”到“人”的函数,比如getMother,那么就能得到getMotherName(p:Person):String=getName(getMother(p))。
  因此,若有一个f:Person=>Person可以作为参数,那么就能动态地产生不同的获取名字的方法,而这种接受函数作为参数的函数称为高阶函数,也是函数组合的主要手段,而函数在函数式编程中也成为了可以独立存在,并成为输入和输出的一等公民:

def getPersonName(p: Person, f:Person=>Person) = getName(f(p))
def getMotherName(p: Person) = getPersonName(p, getMother)
def getFatherName(p: Person) = getPersonName(p, getFather)
def getSelfName(self: Person) = getPersonName(self, p => p) //使用Lambda定义了一个返回自身的匿名函数

  高阶函数除了可以接受函数作为参数外,还可以输出函数作为返回结果,涉及闭包和函数生成器等概念。如:

def sumByN(n: Int):Int=>Int = i => i+n
val sumBy3:Int=>Int = sumByN(3) = i => i+3
sumBy3(2) //return 5
sumBy3(1) //return 4

 高阶函数作为一种组合的手段还会衍生出其它复杂的函数组合方式,相对比较复杂,会在后续文章中解释。如Option[]和Future[]等单子类(Monad)的组合方式:

val o1:Option[Int] = Option(1)
val o2:Option[String] = o1.map(_ * 10).flatmap(i => Option(i.toString())) //使用了Lambda作为匿名函数
o2.get //return "10"

核心2. 引用透明与无副作用

  引用透明,是指每次将同一个参数输入给函数,函数总是能返回同样的输出,因此总是可以将(函数+输入)替换为函数执行的结果,这种无论何时何地的可替换性就称为引用透明Referential Transparency。比如def square(a:Int) = a*a这个函数是引用透明的,因为无论何时,遇到square(2),总是可以用2*2的结果4来替换square(2)。
  若想实现引用透明,那么这个函数一定是无副作用的,或者称纯函数Pure Function。比如下面这个getAge的函数就不是引用透明的,因为它修改了宿主的状态,从而每次调用p.getAge()都不能用它的结果来替换。

class Person(var age:Int) {
    def getAge() {
        age = age + 1
        age
    }
}
val p = new Person(10)
p.getAge() //11
p.getAge() //12

 因此,可以实现下面这个版本来实现同样的功能,却保持无副作用:

class Person(val age:Int) {
    def getAge():(Person, Int) {
        (new Person(age + 1), age + 1)
    }
}
val p1 = new Person(10)
val (p2, p1Age) = p1.getAge() //p1Age = 11
val (p3, p2Age) = p2.getAge() //p2Age = 12
p1.getAge() //依然返回(_,11)

  关于更多如何处理副作用的方式,及其优点会在系列的后续文章中介绍。

核心3. 申明式的风格

 申明式的风格与命令式的风格相对。命令式的风格就像我们编写的传统语言,如C, java等,通过一条条的指令来告诉计算机应该进行什么操作,从而最终实现某一功能。而申明式的风格则相反,强调描述最终要实现的功能的样子,语言背后的类库会帮助我们完成这些功能。一个典型的申明式的例子就是Spring中的注解,我们只是申明了@Autowire,表明此处需要一个某类型的成员变量,而框架和类库会自动帮我们解决这个问题;而用命令式的实现注入,则需要手工创建一个对象,并调用set方法。两者在可读性和抽象层次上都有所不同。
 申明式的风格是强调函数作为一种映射的自然结果。比如,scala的Future就有map方法对Future类型进行映射,可以在Future的结果返回之前,就将原来的Future[X]映射为Future[Y]类型,如下面的代码直接对Future[Int]进行映射,产生了Future[String],最后的结果即为新Future返回的结果,并且主线程不会停止(除非刻意等待这个Future的返回结果):

//scala
val f:Future[Int] = xxx
val f1:Future[Int] = f.map(i => i*10) //还记得对象上的方法能在逻辑上转换为函数吗?等价为: def map(f, i => i*10)
val f2:Future[String] = f1.map(j => j.toString()) //f2的返回即为最终结果

val f2 = f.map(i => i*10).map(j => j.toString())//内联整理之后更加简洁
println("over") //能瞬间运行到这里,因为线程不会停止

  在上面的例子中,我们通过函数的组合来描述功能本身。我们将i => i*10和j => j.toString()这两个函数组合进了map这个函数,因此很直接地申明了我们想要的最终功能:即将一个还未产生的数字*10并转换为String。我们其实并不知道scala的Future.map()是如何将一个还未返回的结果进行变换处理的,甚至不知道这个变换是在哪个线程上执行的,在这方面类库帮了我们很多,就像Spring能@Autowire一样地神奇。因此,整个语言就通过函数的组合表现出了申明式的风格,使得编程本身所面向的抽象层次更高,不再执着于如何实现,而是强调要实现什么。

与面向对象的区别?

1. 函数是一等公民

  在面向对象的语言中,只有类才是一等公民,而所谓函数在其中只能称为类和对象的”方法”。”方法”一词暗指其属于某一对象(除非申明static),而函数则优先将其考虑为全局的,因为它代表了一种将A类对象转换为B类对象的一种客观存在的映射关系。
  上面例子中的getAge()看上去更像是一个”方法”,但任何对象中的”方法”都可以被改写为函数,这两种结构在形式上的等价的。上面这个例子可以改写为:def getAge(p:Person):(Person, Int),如此一来getAge就从隶属于Person类的方法转为全局的函数了。
  在面向对象的语言中,只有对象才是一等公民,因此只能将对象(和数值)作为返回值和参数传递。部分拥有函数式功能的OO语言,如java8和C#,可以将Lambda表达式会被解析为函数接口类型的对象,依然在面向对象的框架下将函数作为对象传递。
  如下面的一段java8代码能返回一个Function类型的对象,虽然能实现类似高阶函数的功能,但本质仍然是对象的传递。

public static Function<String, String> addPost(Function<String, String> f1) {
    return (String s) -> f1.apply(s)+"_post";
}

2. 强调函数的无副作用

  在面向对象的语言中,方法也可以是无副作用的;而在既支持函数式编程又支持面向对象的scala中,函数也可以被误写成带有副作用的。但是scala在语法上更支持无副作用函数。
  无副作用的函数强调这样的函数只是一种映射关系,任何输入的对象在参与计算后都不会/不能被更改,同时执行这种映射关系自然也不会修改函数之外的环境。函数式编程强调的是函数本身,与方法相比,函数本身与所在的类的字段是平级的关系,类的字段属于函数之外的环境,因此是不能被变更的。
  而面向对象则强调以对象为粒度去封装状态,并且时常暴露一些变更的方法,这些方法存在的目的就是操作和变更对象的成员变量。这与函数不变更函数之外的环境是相反的理念。

3. 对函数组合的支持

  对函数组合的支持一方面体现在语法上。虽然scala也用面向对象的手法实现函数的传递,但它看上去更像是函数式编程,能够支持函数类型的申明、匿名函数及类型的快速推导,所以在表现力和简洁性方面远优于java8。下面这段代码是用scala来实现之前java8的例子:

def addPost(f1: String=>String) = s => f1(s)+"_post"

  此外,在语法上Scala等更函数式的语言还提供单子类操作的快捷语法糖:

for {
    f1 <- Future{longIntProducer()} //f1花一段时间后返回一个Int
    f2 <- Future{longIntProcessor(f1)} //f2又花一段时间返回另一个Int
} yield f2 //以Future的形式返回f2的结果

  对函数组合的支持另一方面体现在类库上。比如java也有Future这个类型,但是java的Future并不像scala的Future那样支持map映射,它仅支持线程等待的get操作,通过get操作可以让当前线程等待,并获取Future返回的结果。之前scala的Future的例子用java来实现的话效果会是这样:

//java
Future<Integer> f = xxx;
Integer i = f.get(); //注意!当前线程会停止,直到Future计算结束
Integer j = i * 10; //然后再依次对i进行其它计算
String s = j.toString();

  可以看出面向对象的语言更倾向于命令式的风格,强调按步骤详细描述功能的实现过程,提供的类库也反映出这个现象;而函数式语言提供的类库大多为高阶函数,已经封装了很多高层次的功能,因此整体语言更倾向于申明式的风格,抽象层次更高。

总结

  函数式编程虽然有很多特点和特征,但我认为其本质是面向函数以及函数的组合。它的核心特征是强调函数组合、引用透明以及申明式的风格,因此和面向对象的主要不同点也在于函数的地位、副作用的处理以及命令与申明式的区别上。
  我觉得函数式编程与之前熟悉的面向对象的风格有很大的差异,是编程范畴下的另一种的范式。虽然有一定的学习门槛,但一旦越过后能带来很大的效率提升,有很多的优点值得我们进一步探索。
  因此一方面出于编程效率和技能的原因,建议在生产工作中逐步尝试;然而更重要其背后的思想价值。函数式编程背后有一套数学理论作为支撑,在世界观与认知上与传统编程有很大的不同,如果想要拓展个人的视野和思维方式,那么函数式编程就更有学习的价值了。

应该如何学习?

  如果想再深入地学习函数式编程,建议结合scala这门语言。因为这门语言既支持面向对象及命令式的编程,又支持函数式的申明式编程,功能强大特性丰富。在微观细节上命令式风格的代码更容易理解,但在复杂功能上申明式的风格效率更高更直观,可以说scala能很好地同时具有两者的优点。
  本系列后续第二篇会继续介绍函数式编程的其它特性和优点,这些特性都或多或少地源于它的核心特征。第三篇会介绍如何从面向对象的思维模式切换到函数式的思维模式,重点讲解OO的设计模式在函数式编程的框架上要如何实现。第四篇会简单介绍函数式编程背后与范畴论有关的数学知识,以领略函数式编程的世界观。

猜你喜欢

转载自blog.csdn.net/samsai100/article/details/73195959