函数式编程是指只用纯函数来构造程序, 函数是没有副作用的
先说一下什么是副作用:
普通一个函数是输入一个参数输出一个返回值,而带有副作用的函数不仅仅返回一个值,还带有其它的操作:
比如:
- 修改了变量值
- 直接修改了数据结构
- 创建了一个对象的成员
- 抛出了一个异常或者通过错误停止
- 读取或写入一个文件
- 修改除本身外的其他变量
那么一个纯函数含义是如何来定义的呢?
对于一个输入类型A和输出类型B的函数f(scala里面写为:A=>B 读作 A 到 B) 它是一种将所有A类型的值a关联到某个确切的B类型的值b的运算过程,即b完全由a来决定,任何内部或者外部过程的状态改变都不会影响到f(a)的计算结果,比如:intToString的类型为Int => String,它负责将整数转换为一个相应的字符串,除此之外什么都不用做.
总而言之,一个函数在程序的执行过程中除了根据输入参数给出运算结果之外没有其他的影响,就可以说是没有副作用的.
比如:
代码语言:javascript复制def sum(x : Int,y : Int) : Int = {
x y
}
上述例子中,sum函数,接受两个Int类型的参数,最后将两个参数值相加,那么sum函数就是一个纯函数
代码语言:javascript复制def sum(x:Int,y:Int) : Int = {
println(x y)
x
}
那么这个sum函数,也是接受两个Int类型参数,但是内部println了,将结果输出了,那么它就不是一个纯函数.因为它做了不该它做的事情.
由于可以忽略上下文,引用是透明的,从而,第一,我们可以在任何地方调用函数,并确定其行为与上下文无关,每次的行为都能够确保相同,由于没有任何全局对象被修改,对函数的并发调用也是安全可靠的,不需要任何线程安全的编写技巧
第二:可以用表达式计算得出值替换表达式本身.
变量的不可变性:
"变量"这个词在函数式编程中和在传统的面向对象编程是不同的,面向对象编程认为变量是可变的,然而,在函数式编程中,变量时不可变的.
Scala中的函数式编程
作为一门面向对象与函数式的混合范式语言,Scala并不强制函数必须是纯函数,也并不要求变量时不可变的(但是尽量将变量设为不可变)
在前面几个章节中,我们学习了Scala的高阶函数,,下面来举几个例子:
我们将其组合在一起,用它来对一个整数列表进行遍历,过滤出其中的偶数,对每个偶数乘以 2, 再使用 reduce 函数将各个整数乘在一起:
(1 to 10) filter (_ % 2 == 0) map (_ * 2) reduce (_ * _)
回顾一下, _ % 2 == 0, _ * 2,与 _ * _ 是函数字面量。前两个函数只有一个参数,赋值给占位符 _;最后一个函数带两个参数,该函数本身是 reduce 函数的参数。reduce 函数将各个元素做累乘,也就是说它将整数的集合 reduce 为一个值reduce 函数带两个参数,均赋值给了占位符 _。其中一个参数是集合中的当前元素,另一个参数就是累乘值,是上一次调用 reduce 函数得到的部分元素的累乘结果。(第一个参数是累乘参数,还是第二个参数是累乘参数取决于具体实现)对传入的函数的要求是:其计算必须满足结合律,类似乘法与加法,因为我们不保证集合中元素的计算顺序。
我们只用了一行代码,没有用可变的计数器,也没有用可变变量作为累乘结果,就“循环”遍历了列表得出结果。
匿名函数,Lambda和闭包
在前面的篇章中,我们也了解了匿名函数,Lambda和闭包的知识,在这里我们在详细的叙述一遍.
闭包 :
首先还是上面的例子,我们使用闭包来推演一下:
var factor = 2
val multipliter = (i : Int) => i * factor // 还记得在高阶函数章节中,函数式一等公民,函数可以直接赋值给变量, multipliter 在这里就一个是函数变量
(1 to 10) filter(_ % 2 == 0) map multipliter reduce(_*_) // 输出结果为122880
factor = 3
(1 to 10) filter(_ % 2 == 0) map multipliter reduce(_*_) // 输出结果为933120
首先来解释一下上面这段代码: 生成一个1到10的数组.然后通过filter 过滤掉元素为奇数,通过map函数转换为一个新的数组,然后通过reduce两两相乘.
我们定义一个名为 factor 的变量,作为累乘因子。而之前的匿名函数 _ * 2 则替换为一个名为 multiplier 的变量,变量的值由 factor 决定。注意, multiplier 事实上也是一个函数。由于函数在 Scala 中是第一等的,因此我们定义了表示函数的变量。不过,这不是简单的替换,在这里 multiplier 引用了 factor,而不是将其硬编码为 2。注意看我们使用两个不同的 factor 值时,程序的运行结果。首先我们的输出值为 122880,与之前相同,但接着输出值为 933120。尽管 multiplier 是一个不可变的函数字面量,当 factor 改变时, multiplier 的行为也跟着改变。在 multiplier 函数中有两个变量 i 和 factor。 i 是一个函数的参数,所以每次调用时, i都绑定了一个新的值。然而, factor 并不是 multiplier 的参数,而是一个自由变量,是一个当前作用域中某个值的引用。所以,编译器创建了一个闭包,用于包含(或“覆盖”)multiplier 与它引用的外部变量的上下文信息,从而也就绑定了外部变量本身。这就是 factor 变化时, multiplier 也跟着变化的原因。 Multiplier 引用了 factor,每次调用时都重新读取 factor 的值。如果函数没有外部引用,那它就只是包含了自身,不需要外部上下文信息。
用不闭包的方式来拆分一下:
代码语言:javascript复制var factor = 2
def fun(i : Int) = {
i * factor
}
val multipliter = fun
(1 to 10) filter(_%2==0) map ((i : int) => i * factor) reduce(_*_) // multipliter 在上面充当的就是((i:Int) => i * factor) ,在这里map的时候 multipliter的参数就是i,而i就是filter之后的值之后reduce 将值进行累乘
即使 factor 处于某个局部作用域(如某个方法)中,而我们将 multiplier 传递给其他作用域(如另一个方法)中时,这一机制仍然有效。该自由变量 factor 的有效性一直伴随
multiplier 函数:
代码语言:javascript复制def m1 (multiplier: Int => Int) = {
(1 to 10) filter (_ % 2 == 0) map multiplier reduce (_ * _)
}
def m2: Int => Int = {
val factor = 2
val multiplier = (i: Int) => i * factor
multiplier
}
m1(m2)
我们调用 m2,返回了一个类型为 Int => Int 的函数。返回的内部值是 multiplier,multiplier 引用了 m2 中定义的变量 factor,一旦 m2 返回,就离开了 factor 变量的作用域。
然后调用 m1,将 m2 的返回值传递给它。尽管 factor 变量已经离开了 m1 的作用域,但程序的输出与之前的例子相同,仍为 122880。 m2 返回的函数事实上是一个闭包,它包含了对 factor 的引用。
- 函数
一种具有名或匿名的操作。其代码直到被调用时才执行。在函数的定义中,可能有也可
能没有引用外部的未绑定变量。
- Lambda
一种匿名函数。在它的定义中,可能有也可能没有引用外部的未绑定变量。
- 闭包
是一个函数,可能匿名或具有名称,在定义中包含了自由变量,函数中包含了环境信
息,以绑定其引用的自由变量。