一、函数
1、函数的声明定义和调研
函数是一组一起执行一个任务的语句。 Scala 有函数和方法,二者在语义上的区别很小。Scala 方法是类的一部分,而函数是一个对象可以赋值给一个变量。换句话来说在类中定义的函数即是方法。
我们可以在任何地方定义函数,甚至可以在函数内定义函数(内嵌函数)。更重要的一点是 Scala 函数名可以有以下特殊字符:+, ++, ~, &,-, – , , /, : 等。
Scala也是一种函数式语言,所以函数是 Scala 语言的核心。
1.1函数声明定义
Scala 函数声明格式如下:
def functionName ([参数列表]) : [return type]
如果你不写等于号和方法主体,那么方法会被隐式声明为”抽象(abstract)”,包含它的类型于是也是一个抽象类型。
函数定义
方法定义由一个def 关键字开始,紧接着是可选的参数列表,一个冒号”:” 和方法的返回类型,一个等于号”=”,最后是方法的主体。
Scala 函数定义格式如下:
def functionName ([参数列表]) : [return type] = {
function body
return [expr]
}
以上代码中 return type 可以是任意合法的 Scala 数据类型。参数列表中的参数可以使用逗号分隔。示例:
def addInt( a:Int, b:Int ) : Int = {
var sum:Int = 0
sum = a + b
return sum
}
1.2 函数调用
Scala 提供了多种不同的函数调用方式:
以下是调用方法的标准格式:
functionName( 参数列表 )
如果函数使用了实例的对象来调用,我们可以使用类似java的格式 (使用 . 号),跟方法调用一样:
[instance.]functionName( 参数列表 )
例如上节的调用:
scala> addInt(1,2)
res0: Int = 3
1.3 函数字面量
函数字面量又称为lambda表达式, 使用=>符号定义:
scala> var fun = (x:Int) => x + 1
fun: Int => Int = <function1>
scala> fun(7)
res4: Int = 8
函数字面量是一个对象, 可以作为参数和返回值进行传递.
使用_逐一替换普通函数中的参数 可以得到函数对应的字面量:
scala> def add(x:Int, y:Int):Int = {return x + y}
add: (x: Int, y: Int)Int
scala> add(7,8)
res5: Int = 15
scala> var fun = add(_,_)
fun: (Int, Int) => Int = <function2>
scala> fun(7,8)
res6: Int = 15
2、函数参数
根据函数的输入参数可以分为:带名参数、默认参数、可变参数,当然几个参数是可以适当组合使用的。
2.1 带名参数
一般情况下函数调用参数,就按照函数定义时的参数顺序一个个传递。但是我们也可以通过指定函数参数名,并且不需要按照顺序向函数传递参数,实例如下:
def chufa( a:Double, b:Double ) : Double = {
var value:Double = 0.0
value = a/b
return value
}
//改变a,b顺序,值不变
scala> chufa(b=2,a=3)
res10: Double = 1.5
scala> chufa(a=3,b=2)
res11: Double = 1.5
//不能在参数以外赋值给参数,否则无效
scala> var a=3 ;var b=2;chufa(a,b)
a: Int = 3
b: Int = 2
res13: Double = 1.5
scala> var a=3 ;var b=2;chufa(b,a)
a: Int = 3
b: Int = 2
res15: Double = 0.6666666666666666
2.2 默认参数
Scala 可以为函数参数指定默认参数值,使用了默认参数,你在调用函数的过程中可以不需要传递参数,这时函数就会调用它的默认参数值,如果传递了参数,则传递值会取代默认值。实例如下:
def chufa( a:Double=5, b:Double =7) : Double = {
var value:Double = 0.0
value = a/b
return value
}
scala> chufa()
res17: Double = 0.7142857142857143
//赋值以后
scala> chufa(2,5)
res19: Double = 0.4
2.3 变长参数
Scala 允许你指明函数的最后一个参数可以是重复的,即我们不需要指定函数参数的个数,可以向函数传入可变长度参数列表。
Scala 通过在参数的类型之后放一个星号来设置可变参数(可重复的参数)。例如:
def leiji( a:Int*) : Int = {
var value:Int = 1
for(i <- a){
value=value*i
}
return value
}
//累乘积,可输入多参数
scala> leiji(2)
res31: Int = 2
scala> leiji(2,3,4)
res32: Int = 24
//如果,想这个参数当作参数序列处理,追加 :_*
scala> leiji(1 to 5)
<console>:24: error: type mismatch;
found : scala.collection.immutable.Range.Inclusive
required: Int
leiji(1 to 5)
scala> leiji(1 to 5:_*)
res35: Int = 120
//与其他参数形式结合使用
def leiji( a:Int,b:Int*) : Int = {
var value:Int = a
for(i <- b){
value=value*i
}
return value
}
3、常见函数
3.1 递归函数
递归函数在函数式编程的语言中起着重要的作用。
Scala 同样支持递归函数。
递归函数意味着函数可以调用它本身。
def factorial(n: BigInt): BigInt = {
if (n <= 1)
1
else
n * factorial(n - 1)
}
//计算阶乘
scala> factorial(5)
res0: BigInt = 120
3.2 函数嵌套
我们可以在 Scala 函数内定义函数,定义在函数内的函数称之为局部函数。
以下实例我们实现阶乘运算,并使用内嵌函数:
def factorial(i: Int): Int = {
def fact(i: Int, accumulator: Int): Int = {
if (i <= 1)
accumulator
else
fact(i - 1, i * accumulator)
}
fact(i, 1)
}
scala> factorial(4)
res2: Int = 24
3.3 匿名函数
Scala 中定义匿名函数的语法很简单,箭头左边是参数列表,右边是函数体。
使用匿名函数后,我们的代码变得更简洁了。
下面的表达式就定义了一个接受一个Int类型输入参数的匿名函数:
var inc = (x:Int) => x+1
scala> inc(8)
res3: Int = 9
上述定义的匿名函数,其实是下面这种写法的简写:
def add2 = new Function1[Int,Int]{
def apply(x:Int):Int = x+1;
}
4 高阶函数
高阶函数(Higher-Order Function)就是操作其他函数的函数。
Scala 中允许使用高阶函数, 高阶函数可以使用其他函数作为参数,或者使用函数作为输出结果。
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)
res7: Array[Int] = Array(2, 3, 4)
上述示例将arr中每个元素执行了x=>x+1操作, 结果组成了一个新的集合返回.
用map打印一个三角形:
scala> (1 to 9).map("*" *_).foreach(println _)
*
**
***
****
*****
******
*******
********
*********
上述示例用到了foreach,它的函数并不返回任何值,只是简单地将函数应用到每个元素而已。
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))
res8: Array[Int] = Array(1, -1, 2, -2, 3, -3)
scala> arr.flatMap(x=>Array(x,x*2))
res9: Array[Int] = Array(1, 2, 2, 4, 3, 6)
上述示例将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并不保证遍历的顺序, 若要求特定顺序请使用reduceLeft或reduceRight.(注意:相应的x,y的顺序。见下例:)
scala> arr.reduceLeft((x,y)=>x-y)
res14: Int = -4
#(1-2)-3
scala> arr.reduceRight((x,y)=>x-y)
res15: Int = 2
#(2-1),3 3-(2-1)
scala> arr.reduceRight((x,y)=>y-x)
res17: Int = 0
#(3-2)-1 仔细体会
scala> arr.reduceLeft((x,y)=>y-x)
res18: Int = 2
#3-(2-1)
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))
filter方法交出所有匹配某个特定条件的元素。
scala> (1 to 9).filter(_ % 3==0)
res25: scala.collection.immutable.IndexedSeq[Int] = Vector(3, 6, 9)
scala> (1 to 9).filter(_ % 3==1)
res26: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 4, 7)
下面是一个用二元函数排序对英文句子按照词汇长度的排序:
scala> "Mary had a little lamb".split(" ").sortWith(_.length < _.length)
res27: Array[String] = Array(a, had, Mary, lamb, little)
高阶函数实际上是自定义了控制结构:
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)
res21: Int = 16
twice函数定义了将函数调用两次的控制结构, 因此实参2被应用了两次x=>x*x得到16.
5、 函数传名调用(call-by-name,也叫换名传递)
Scala的解释器在解析函数参数(function arguments)时有两种方式:
传值调用(call-by-value):先计算参数表达式的值,再应用到函数内部;
传名调用(call-by-name):将未计算的参数表达式直接应用到函数内部
在进入函数内部前,传值调用方式就已经将参数表达式的值计算完毕,而传名调用是在函数内部进行参数表达式的值计算的。
这就造成了一种现象,每次使用传名调用时,解释器都会计算一次表达式的值。
换名传递可以用于实现惰性取值的效果.
换名传递参数用: =>代替:声明, 注意空格不能省略
def work():Int = {
println("generating data");
return (System.nanoTime % 1000).toInt
}
def delay(t: => Int) {
println(t);
println(t);
}
scala> delay(work())
generating data
780
generating data
5
从结果中可以注意到work()函数被调用了两次, 并且换名参数t的值发生了改变.
换名参数只是传递时机不同,仍然采用val的方式进行传递.
6、部分应用函数与偏函数
使用_代替函数参数的过程中,如果只替换部分参数的话则会得到一个新函数, 称为部分应用函数(Partial Applied Function):
scala> val increase = add(_:Int, 1)
increase: Int => Int = <function1>
scala> increase(5)
res29: Int = 6
偏函数是一个数学概念, 是指对定义域中部分值没有定义返回值的函数:
def pos = (x:Int) => x match {
case x if x > 0 => 1
}
scala> pos(7)
res30: Int = 1
8、函数的柯里化(currying)
函数的柯里化(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)
res31: Int = 5
scala> add(2)
res32: Int => Int = <function1>
可以注意到add(2)返回的仍是函数.
scala提供了柯里化函数的简化写法:
scala> def add(x:Int)(y:Int)={x+y}
add: (x: Int)(y: Int)Int
9、函数简化示例
//花括方式(写法3)
scala> Array(1,2,3,4).map{(x:Int)=>x+1}.mkString(",")
res25: String = 2,3,4,5
//省略.的方式(写法4)
scala> Array(1,2,3,4) map{(x:Int)=>x+1} mkString(",")
res26: String = 2,3,4,5
//参数类型推断写法(写法5)
scala> Array(1,2,3,4) map{(x)=>x+1} mkString(",")
res27: String = 2,3,4,5
//函数只有一个参数的话,可以省略()(写法6)
scala> Array(1,2,3,4) map{x=>x+1} mkString(",")
res28: String = 2,3,4,5
//如果参数右边只出现一次,则可以进一步简化(写法7)
scala> Array(1,2,3,4) map{_+1} mkString(",")
res29: String = 2,3,4,5
//值函数简化方式
//val fun0=1+_,该定义方式不合法,因为无法进行类型推断
scala> val fun0=1+_
<console>:10: error: missing parameter type for expanded function ((x$1) => 1
x$1))
//值函数简化方式(正确方式)
scala> val fun1=1+(_:Double)
un1: Double => Double = <function1>
scala> fun1(999)
es30: Double = 1000.0
//值函数简化方式(正确方式2)
scala> val fun2:(Double)=>Double=1+_
fun2: Double => Double = <function1>
scala> fun2(200)
res31: Double = 201.0
二、闭包(Closure)
闭包是一个函数,返回值依赖于声明在函数外部的一个或多个变量。
闭包通常来讲可以简单的认为是可以访问一个函数里面局部变量的另外一个函数。
如下面这段匿名的函数:
//(x:Int)=>x+more,这里面的more是一个自由变量(Free Variable),more是一个没有给定含义的不定变量
//而x则的类型确定、值在函数调用的时候被赋值,称这种变量为绑定变量(Bound Variable)
scala> (x:Int)=>x+more
<console>:8: error: not found: value more
(x:Int)=>x+more
^
scala> var more=1
more: Int = 1
scala>val fun=(x:Int)=>x+more
fun: Int => Int = <function1>
scala> fun(10)
res1: Int = 11
scala> more=10
more: Int = 10
scala> fun(10)
res2: Int = 20
//像这种运行时确定more类型及值的函数称为闭包,more是个自由变量,在运行时其值和类型得以确定
//这是一个由开放(free)到封闭的过程,因此称为闭包
scala> val someNumbers = List(-11, -10, -5, 0, 5, 10)
someNumbers: List[Int] = List(-11, -10, -5, 0, 5, 10)
scala> var sum = 0
sum: Int = 0
scala> someNumbers.foreach(sum += _)
scala> sum
res8: Int = -11
scala> someNumbers.foreach(sum += _)
scala> sum
res10: Int = -22
//下列函数也是一种闭包,因为在运行时其值才得以确定
def multiplyBy(factor:Double)=(x:Double)=>factor*x
闭包是函数式编程的重要概念,详细理解可参考:
https://zhuanlan.zhihu.com/p/21346046
三、函数式编程
简单介绍一下函数式编程范式.
函数式编程中, 函数是从参数到返回值的映射而非带有返回值的子程序; 变量(常量)也只是一个量的别名而非内存中的存储单元.
也就是说函数式编程关心从输入到输出的映射, 不关心具体执行过程. 比如使用map对集合中的每个元素进行操作, 可以使用for循环进行迭代, 也可以将元素分发到多个worker进程中处理.
函数式编程可理解为将函数(映射)组合为大的函数, 最终整个程序即为一个函数(映射). 只要将数据输入程序, 程序就会将其映射为结果.
这种设计理念需要满足两个特性. 一是高阶函数, 它允许函数进行复合; 另一个是函数的引用透明性, 它使得结果不依赖于具体执行步骤只依赖于映射关系.
结果只依赖输入不依赖上下文的特性称为引用透明性; 函数对外部变量的修改被称为副作用.只通过参数和返回值与外界交互的函数称为纯函数,纯函数拥有引用透明性和无副作用性.
不可变对象并非必须, 但使用不可变对象可以强制函数不修改上下文. 从而避免包括线程安全在内很多问题.
函数式编程的特性使得它拥有很多优势:
函数结果只依赖输入不依赖于上下文, 使得每个函数都是一个高度独立的单元, 便于进行单元测试和除错.
函数结果不依赖于上下文也不修改上下文, 从而在并发编程中不需要考虑线程安全问题, 也就避免了线程安全问题带来的风险和开销. 这一特性使得函数式程序很容易部署于并行计算和分布式计算平台上.
函数式编程在很多技术社区都是有着广泛争议的话题, 笔者认为”什么是函数编程”,”函数式编程的精髓是什么”这类问题并不重要。
作为程序员应该考虑的是”函数式编程适合解决什么问题?它有何有缺?”以及”何时适合应用函数式编程?这个问题中如何应用函数式编程?”.
函数式编程并非”函数式语言”的专利. 目前包括Java,Python在内的, 越来越多的语言开始支持函数式特性, 我们同样可以在Java或Python项目上发挥函数式编程的长处.
参考资料:1、《快学scala》
2、http://www.runoob.com/scala/scala-functions.html
3、https://www.cnblogs.com/Finley/p/6386192.html
4、http://blog.csdn.net/lovehuangjiaju/article/details/46992275