有趣的Scala函数式编程

322 阅读8分钟

Scala的函数

1.原始定义

Scala中的函数最原始的定义样式为:

def 函数名(传入参数:参数类型):结果类型 = {方法体}
例:
1.有参,有返回值
def SelfAdd1(value:Int):Int = {
    value += 1
    value //return可以省略,Scala会使用函数体最后一行作为返回值
}
​
2.有参,无返回值
def SelfAdd2(value:Int):Unit = {
    value += 1
}
​
3.无参,无返回值
def SelfAdd3():Unit = {
    println("无参,无返回值")
}
/*
返回类型为Unit时或者能推断出来时,可以省略返回类型
无参可以省略括号 但是定义时省略,调用时也要省略,定义时未省,调用可省可不省
函数体只有一行,可省略大括号
建议定义时括号不要省略
*/
等价于
def selfAdd3 = println("无参,无返回值")
/*
上下同理,不一一简化
*/
4.无参,有返回值
def SelfAdd4():String = {
    "无参,有返回值"
}

对于传入的参数,首先允许传入多个参数,其次,允许传入可变参数(即传入的参数的数量可变,参数类型后加 * 表示可接收多个参数),若传入的参数列表中存在多个参数,则可变参数放置在末尾

例:
1.可变参数
def test1(s:String *):Unit = {
    println(s)
}
​
test1("hello")
test1("hello","scala")
​
2.传入多个参数,存在可变参数
def test2(name:String, s:String):Unit ={
    println(name + s)
}
​
test2("xf","hello")
test2("xf","don't","know")

2.函数变量

在C++和Java中,类似的Scala原始函数的定义方式即为他们定义函数的唯一方法,而在Scala中,函数和其他对象处于同等级别的“头等公民”,函数有“类型”和“值”的区分,类型来明确函数接受多少个参数,而值则是函数的具体实现,故对于函数,我们可以把它当做变量来看待,即转化为函数变量,定义为:

val/var 函数名:参数类型=>结果类型={传入参数=>具体方法}
即:
val/var 函数名:类型 = {值}

上述对于函数变量的定义遵循了Scala的定义变量的基本语法,即:var/val 变量名=初始值,不同的是,这个变量是函数类型的,我们之前定义的变量,类型一般可以是Int、String等,而函数变量,相当于给定义的值赋了一种新的类型,按照上面定义这个标准,我们首先定义一个简单的函数变量:

val counter:Int=>Int = {value=>value+1}

而得益于Scala特有的类型推断机制,对于上面这个函数变量,我们可以简写为下面这样:

val counter = (value:Int) => value+1

看到这里,我们可能有点疑惑,不是说标准形式是 函数名:参数类型=>结果类型 的嘛,为什么这里省略了 Int=>Int ,而后面传入的参数value却给了Int类型呢,这就要提到Scala中的匿名函数了,也就是lambda表达式。

定义是这样的:

没有名称的函数是匿名函数,也称为函数文字 。 当用户想要创建内联函数时,使用这种类型的函数。

像我们出现在等式右边的 (value:Int)=>value 就是一个匿名函数,可以这样理解,这个函数的左边是传入参数和参数类型,右边是方法体。当然,我们知道了后面这一部分是匿名函数好像还是没有解决为什么省略掉了前面的函数名后面的 Int=>Int,前面就说过Scla拥有强大的类型2推断机制,因为我们后面对于传入参数value给的参数类型为Int,所以系统推断出 value+1 的类型也为Int, 故推断出counter函数变量的类型为 Int=>Int ,所以可以直接省略,再举两个例子:

val add = (a:int,b:int)=>a+b
/* 同样的,拆分来看,前面add是函数变量名,后面(a:int,b:int)代表传入参数为两个,a+b则为方法体,通过a,b的类型推断出a+b的类型,继而推断函数变量的类型*/
val show = (s:String)=>println(s)
/* 这里的println()为输出语句,方法体类没有具体值的类型,故此时结果类型为Unit,即此函数变量的类型为 String=>Unit*/

补充一下,函数变量的调用与函数的调用一致,即直接函数名(参数),当然,如果定义函数时无传入参数,定义的函数可以直接使用函数名调用

例:

def show = println("hello")
show
//输出结果即为hello

Scala函数的一大特点我认为是能省就省,越简洁越好,所以,对于上面这种使用匿名函数和Scala类型推断机制所简化的函数变量表达式好像还是没有那么简洁,所以,又有了一个定义:如果函数的每个参数在函数的字面量内仅出现一次,那么可以省略“=>”,并使用占位符”_”来简化函数字面量的表示,第一个下划线表示第一个参数,第二个下划线表示第二个参数,以此类推。 而这个定义又能怎样简化我们的函数呢,我们看下面这个例子:

val counter = (value:Int) => value+1
​
val counter = (_:Int) + 1

可能看到这个式子有点懵,不着急,我们一步步来看,首先,我们把出现一次的参数用占位符替代:

val counter = (_:Int) => _+1 
每个参数在函数的方法体中只出现了一次(在方法体内,传入参数的这边不算哦),可以省略 =>
故:
val counter = (_:Int) _+1
直接合并一下
val counter = (_:Int) + 1

一个参数的没有问题,我们来看一下多个参数的例子,相信看一下就明白了:

val Add = (_:Int) + (_:Int)
我们试着从这个例子回推一下原函数变量
val Add = (a:Int,b:Int) => a+b

通过对比,我们可以发现,通过占位符,我们一下子省了好多代码量,而且不用头秃的去想传入参数的名称,对于简单的函数的编写简直是救星般的存在。

3.高阶函数

在Scala中,函数可以像其他类型的值一样被传递和操作,即其可作为其他函数的参数或者返回值,而当一个函数包含其他函数作为其参数或者返回结果时,该函数即被称为高阶函数。

听起来好像还蛮简单的,话不多说,我们直接上代码:

def sum( f:Int=>Int,a:Int,b:Int):Int = {
    if (a > b) 0
    else f(a) + sum(f, a+1, b)
}

观察这个函数,可以发现,在传入的参数中,有 f:Int=>Int 这个参数,而这个参数代表的为一个传入类型为Int,结果类型Int的函数,我们可以类比来看,当我们把 f 与 a 对照,而对于 a ,“ : ”后面为该变量的类型,而类比来看,f 后面所跟随的同样为 f 的类型,即Int=>Int型,我们要记住,在Scala中函数变量与变量属于同等级别,我们要一视同仁。

有的人可能不太清楚了,这样写究竟有怎么样的好处呢,我们继续看。

首先,在这个函数的内部,我们发现在else语句块,调用了我们传入的函数参数,同时,很简单的可以看出这是一个迭代函数,我们把函数参数(即f)又传入了下一轮的迭代中。而这种定义的方式可以很方便的将函数块运行逻辑相似的函数统一处理,我们只需给它对应的规则,他就能运行出我们想要的结果。比如上面这个函数,有以下的运行场景:

sum(x=>x,2,4)
res1: Int = 9   (Scala环境的运行结果)
这样的运行结果为求2-4的累加
同样,这里传入的参数为什么是x=>x呢,我们类比来看,上面定义函数时,我们在函数名后面的括号里是这样写的,f:Int=>Int,a:Int,b:Int ,我们看在运行sum时,先看后面两个变量的传入参数,是24,在这里这是a和b分别所对应的值,而对于函数变量,它的值在哪里,如果忘了,我们可以看一下前面函数变量的定义, 即: val/var 函数名:类型 = {值} ,故此时,我们定义的该函数的值为 x=>x 

sum(x=>x*x,2,4)
res2: Int = 29
这样的运行结果为求2-4的平方和


def powersOfTwo(x:Int) : Int ={
    if(x == 0) 1
    else 2 * powersOfTwo(x-1)
}
sum(powersOfTwo,1,5)
res2: Int = 62
这样的运行结果为求1-5的2的幂次和,即2^1+2^2+...+2^5
而对于我们已经定义好的函数,当我们当做参数传入高阶函数内时,参数的值使用参数名即可

这样,我们通过定义了一个高阶函数,解决了多种应用场景的问题,也大大减少了我们代码的编写量。

\