Scala函数与函数式编程

时间:2024-05-22 18:05:38

函数是scala的重要组成部分, 本文将探讨scala中函数的应用.

scala作为支持函数式编程的语言, scala可以将函数作为对象即所谓"函数是一等公民".

函数定义

scala源文件中可以定义两类函数:

  • 类方法: 类声明时定义, 由类实例进行调用

  • 局部函数: 在函数内部定义, 作用域只限于定义它的函数内部

这里只关注函数定义相关内容, 关于类的有关内容请参考面向对象的相关内容.

scala使用def关键字定义函数:

def test() {
println("Hello World!");
}

因为是静态类型语言, 定义含参数和返回值的函数需要指定类型, 语法略有不同:

def add(x:Int, y:Int): Int = {
return x + y;
}

scala支持默认参数:

def add(x:Int = 0, y:Int = 0):Int = {
return x + y;
}

可以指定最后一个参数为可变参数, 从而接受数目不定的同类型实参:

scala> def echo (args: String *) { for (arg <- args) println(arg) }

scala> echo("Hello", "World")
Hello
World

String *类型的参数args实际上是一个Array[String]实例, 但是不能将一个Array作为参数传给args.

若需传递Array作为实参,需要使用arr :_*传递实参:

scala> val arr= Array("Hello" , "World")
arr: Array[String] = Array(Hello, World) scala> echo(arr: _*)
Hello
World

命名参数允许以任意顺序传入参数:

scala> def speed(dist:Double, time:Double):Double = {return dist / time}

scala> speed(time=2.0, dist=12.2)
res28: Double = 6.1

参数传递

scala的参数传递采用传值的方式, 参数被当做常量val而非变量var传入.

当我们试图编写一个swap函数时,出现错误:

scala> def swap(x:Int, y:Int) {var t = x; x = y; y = t;}
<console>: error: reassignment to val
def swap(x:Int, y:Int) {var t = x; x = y; y = t;}
^
<console>: error: reassignment to val
def swap(x:Int, y:Int) {var t = x; x = y; y = t;}
^

scala中的标识符实际是引用而非对象本身, 这一点与Java相同。 类实例中的属性和容器的元素实际上只保存了引用, 并非将成员自身保存在容器中。

不熟悉Java的同学可以将对象和引用类比为C中的变量和指针

val将一个对象设为常量, 使得我们无法修改其中保存的引用,但是允许我们修改其引用的其它对象.

以二维数组val arr = Array(1,2,3)为例。 因为arr为常量,我们无法修改arr使其为其它值, 但我们可以修改arr引用的对象arr(0)使其为其它值:

scala> val arr = Array(1,2,3)
arr: Array[Int] = Array(1, 2, 3) scala> arr = Array(2,3,4)
<console>:12: error: reassignment to val
arr = Array(2,3,4)
^
scala> arr(0) = 2
arr: Array[Int] = Array(2, 2, 3)

参数传递过程同样满足这个性质:

scala> var arr = Array(1,2,3)
arr: Array[Int] = Array(1, 2, 3) scala> def fun(arr:Array[Int]):Array[Int] = {arr(0) += 1; return arr;}
fun: (arr: Array[Int])Array[Int] scala> fun(arr)
res: Array[Int] = Array(3, 2, 3) scala> arr
arr: Array[Int] = Array(3, 2, 3)

换名传递

上述参数传递采用传值的方式传递: 在函数调用时实参值被传入函数执行过程中参数值不会因为实参值改变而发生改变。

换名传递则不立即进行参数传递, 只有参数被访问时才会去取实参值, 即形参成为了实参的别名.

换名传递可以用于实现惰性取值的效果.

换名传递参数用: =>代替:声明, 注意空格不能省略.

def work():Int = {
println("generating data");
return (System.nanoTime % 1000).toInt
} def delay(t: => Int) {
println(t);
println(t);
} scala> delay(work())
generating data
247
generating data
143

从结果中可以注意到work()函数被调用了两次, 并且换名参数t的值发生了改变.

换名参数只是传递时机不同,仍然采用val的方式进行传递.

函数字面量

函数字面量又称为lambda表达式, 使用=>符号定义:

scala> var fun = (x:Int) => x + 1
fun: Int => Int = $$Lambda$1422/1621418276@3815c525

函数字面量是一个对象, 可以作为参数和返回值进行传递.

使用_逐一替换普通函数中的参数 可以得到函数对应的字面量:

scala> def add(x:Int, y:Int):Int = {return x + y}
add: (x: Int, y: Int)Int scala> var fun = add(_,_)
fun: (Int, Int) => Int = $$Lambda$1423/1561881364@37b117dd

部分应用函数与偏函数

使用_代替函数参数的过程中,如果只替换部分参数的话则会得到一个新函数, 称为部分应用函数(Partial Applied Function):

scala> val increase = add(_:Int, 1)
increase: Int => Int = $$Lambda$1453/981330853@78fc5eb

偏函数是一个数学概念, 是指对定义域中部分值没有定义返回值的函数:

def pos = (x:Int) => x match {
case x if x > 0 => 1
}

高阶函数

函数字面量可以作为参数或返回值, 接受函数字面量作为参数的函数称为高阶函数.

scala内置一些高阶函数, 用于定义集合操作:

collection.map(func)将集合中每一个元素传入func并将返回值组成一个新的集合作为map函数的返回值:

scala> var arr = Array(1,2,3)
arr: Array[Int] = Array(1, 2, 3) scala> arr.map(x=>x+1)
res: Array[Int] = Array(2, 3, 4)

上述示例将arr中每个元素执行了x=>x+1操作, 结果组成了一个新的集合返回.

collection.flatMap(func)类似于map, 只不过func返回一个集合, 它们的并集作为flatMap的返回值:

scala> var arr = Array(1,2,3)
arr: Array[Int] = Array(1, 2, 3) scala> arr.flatMap(x=>Array(x,-x))
res: Array[Int] = Array(1, -1, 2, -2, 3, -3)

上述示例将arr中每个元素执行x=>Array(x, -x)得到元素本身和它相反数组成的数组,最终得到所有元素及其相反数组成的数组.

collection.reduce(func)中的func接受两个参数, 首先将集合中的两个参数传入func,得到的返回值作为一个参数和另一个元素再次传入func, 直到处理完整个集合.

scala> var arr = Array(1,2,3)
arr: Array[Int] = Array(1, 2, 3) scala> arr.reduce((x,y)=>x+y)
res: Int = 6

上述示例使用reduce实现了集合求值. 实际上, reduce并不保证遍历的顺序, 若要求特定顺序请使用reduceLeftreduceRight.

zip函数虽然不是高阶函数,但是常和上述函数配合使用, 这里顺带一提:

scala> var arr1 = Array(1,2,3)
arr1: Array[Int] = Array(1, 2, 3) scala> var arr2 = Array('a', 'b', 'c')
arr2: Array[Char] = Array(a, b, c) scala> arr1.zip(arr2)
res: Array[(Int, Char)] = Array((1,a), (2,b), (3,c))

高阶函数实际上是自定义了控制结构:

scala> def twice(func: Int=>Int, x: Int):Int = func(func(x))
twice: (func: Int => Int, x: Int)Int scala> twice(x=>x*x, 2)
res: Int = 16

twice函数定义了将函数调用两次的控制结构, 因此实参2被应用了两次x=>x*x得到16.

柯里化

函数的柯里化(currying)是指将一个接受n个参数的函数变成n个接受一个参数的函数.

以接受两个参数的函数为例,第一个函数接受一个参数 并返回一个接受一个参数的函数.

原函数:

scala> def add(x:Int, y:Int):Int = {return x+y}
add: (x: Int, y: Int)Int

进行柯里化:

scala> def add(x:Int)= (y:Int)=>x*y
add: (x: Int)Int => Int

这里没有指明返回值类型, 交由scala的类型推断来决定. 调用柯里化函数:

scala> add(2)(3)
res10: Int = 6 scala> add(2)
res11: Int => Int = $$Lambda$1343/1711349692@51a65f56

可以注意到add(2)返回的仍是函数.

scala提供了柯里化函数的简化写法:

scala> def add(x:Int)(y:Int)={x+y}
add: (x: Int)(y: Int)Int

本文介绍了一些关于scala函数式编程(functional programming, FP)的特性, 在这里简单介绍一下函数式编程范式.

函数式编程中, 函数是从参数到返回值的映射而非带有返回值的子程序; 变量(常量)也只是一个量的别名而非内存中的存储单元.

也就是说函数式编程关心从输入到输出的映射, 不关心具体执行过程. 比如使用map对集合中的每个元素进行操作, 可以使用for循环进行迭代, 也可以将元素分发到多个worker进程中处理.

函数式编程可理解为将函数(映射)组合为大的函数, 最终整个程序即为一个函数(映射). 只要将数据输入程序, 程序就会将其映射为结果.

这种设计理念需要满足两个特性. 一是高阶函数, 它允许函数进行复合; 另一个是函数的引用透明性, 它使得结果不依赖于具体执行步骤只依赖于映射关系.

结果只依赖输入不依赖上下文的特性称为引用透明性; 函数对外部变量的修改被称为副作用.只通过参数和返回值与外界交互的函数称为纯函数,纯函数拥有引用透明性和无副作用性.

不可变对象并非必须, 但使用不可变对象可以强制函数不修改上下文. 从而避免包括线程安全在内很多问题.

函数式编程的特性使得它拥有很多优势:

  • 函数结果只依赖输入不依赖于上下文, 使得每个函数都是一个高度独立的单元, 便于进行单元测试和除错.

  • 函数结果不依赖于上下文也不修改上下文, 从而在并发编程中不需要考虑线程安全问题, 也就避免了线程安全问题带来的风险和开销. 这一特性使得函数式程序很容易部署于并行计算和分布式计算平台上.

函数式编程在很多技术社区都是有着广泛争议的话题, 笔者认为"什么是函数编程","函数式编程的精髓是什么"这类问题并不重要。

作为程序员应该考虑的是"函数式编程适合解决什么问题?它有何有缺?"以及"何时适合应用函数式编程?这个问题中如何应用函数式编程?".

函数式编程并非"函数式语言"的专利. 目前包括Java,Python在内的, 越来越多的语言开始支持函数式特性, 我们同样可以在Java或Python项目上发挥函数式编程的长处.