每天半小时掌握Scala(day 03)
Posted 爱上终身学习
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了每天半小时掌握Scala(day 03)相关的知识,希望对你有一定的参考价值。
===函数(下)===
函数与操作符:
从技术层面上来说,scala没有操作符重载,因为它根本没有传统意义上的操作符。诸如“+”、“-”、“*”、“/”这样的操作符,其实调用的是方法。方法被当作操作符使用时,根据使用方式的不同,可以分为:中缀标注(操作符)、前缀标注、后缀标注。
中缀标注:中缀操作符左右分别有一个操作数。方法若只有一个参数(实际上是两个参数,因为有一个隐式的this),调用的时候就可以省略点及括号。实际上,如果方法有多个显式参数,也可以这样做,只不过你需要把参数用小括号全部括起来。如果方法被当作中缀操作符来使用(也即省略了点及括号),那么左操作数是方法的调用者,除非方法名以冒号“:”结尾(此时,方法被右操作数调用)。另外,scala的中缀标注不仅可以在操作符中存在,也可以在模式匹配、类型声明中存在,参见相应部分。
前缀标注:前缀操作符只有右边一个操作数。但是对应的方法名应该在操作符字符上加上前缀“unary_”。标识符中能作为前缀操作符用的只有+、-、!和~。
后缀标注:后缀操作符只有左边一个操作数。任何不带显式参数的方法都可以作为后缀操作符。
在scala中,函数的定义方式除了作为对象成员函数的方法之外,还有内嵌在函数中的函数,函数字面量和函数值。
嵌套定义的函数:
嵌套定义的函数也叫本地函数,本地函数仅在包含它的代码块中可见。
函数字面量:
在scala中,你不仅可以定义和调用函数,还可以把它们写成匿名的字面量,也即函数字面量,并把它们作为值传递。函数字面量被编译进类,并在运行期间实例化为函数值(任何函数值都是某个扩展了scala包的若干FunctionN特质之一的类的实例,如Function0是没有参数的函数,Function1是有一个参数的函数等等。每一个FunctionN特质有一个apply方法用来调用函数)。因此函数字面量和值的区别在于函数字面量存在于源代码中,而函数值作为对象存在于运行期。这个区别很像类(源代码)和对象(运行期)之间的关系。
以下是对给定数执行加一操作的函数字面量:
(x: Int) => x + 1
其中,=>指出这个函数把左边的东西转变为右边的东西。在=>右边,你也可以使用{}来包含代码块。
函数值是对象,因此你可以将其存入变量中,这些变量也是函数,你可以使用通常的括号函数调用写法调用它们。如:
val fun = (x: Int) => x + 1
val a = fun(5)
有时,scala编译器可以推断出函数字面量的参数类型,因此你可以省略参数类型,然后你也可以省略参数外边的括号。如:
(x) => x + 1
x => x + 1
如果想让函数字面量更简洁,可以把通配符“_”当作单个参数的占位符。如果遇见编译器无法识别参数类型时,在“_”之后加上参数类型声明即可。如:
List(1,2,3,4,5).filter(_ > 3)
val fun = (_: Int) + (_: Int)
部分应用函数:
你还可以使用单个“_”替换整个参数列表。例如可以写成:
List(1,2,3,4,5).foreach(println(_))
或者更好的方法是你还可以写成:
List(1,2,3,4,5).foreach(println _)
以这种方式使用下划线时,你就正在写一个部分应用函数。部分应用函数是一种表达式,你不需要提供函数需要的所有参数,代之以仅提供部分,或不提供所需参数。如下先定义一个函数,然后创建一个部分应用函数,并保存于变量,然后该变量就可以作为函数使用:
def sum(a: Int, b: Int, c: Int) = a + b + c
val a = sum _
println(a(1,2,3))
实际发生的事情是这样的:名为a的变量指向一个函数值对象,这个函数值是由scala编译器依照部分应用函数表达式sum _,自动产生的类的一个实例。编译器产生的类有一个apply方法带有3个参数(之所以带3个参数是因为sum _表达式缺少的参数数量为3),然后scala编译器把表达式a(1,2,3)翻译成对函数值的apply方法的调用。你可以使用这种方式把成员函数和本地函数转换为函数值,进而在函数中使用它们。不过,你还可以通过提供某些但不是全部需要的参数表达一个部分应用函数。如下,此变量在使用的时候,可以仅提供一个参数:
val b = sum(1, _: Int, 3)
如果你正在写一个省略所有参数的部分应用函数表达式,如println _或sum _,而且在代码的那个地方正需要一个函数,你就可以省略掉下划线(不是需要函数的地方,你这样写,编译器可能会把它当作一个函数调用,因为在scala中,调用无副作用的函数时,默认不加括号)。如下代码就是:
List(1,2,3,4,5).foreach(println)
闭包:
闭包是可以包含自由(未绑定到特定对象)变量的代码块;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。比如说,在函数字面量中使用定义在其外的局部变量,这就形成了一个闭包。如下代码foreach中就创建了一个闭包:
var sum = 0
List(1,2,3,4,5).foreach(x => sum += x)
在scala中,闭包捕获了变量本身,而不是变量的值。变量的变化在闭包中是可见的,反过来,若闭包改变对应变量的值,在外部也是可见的。
尾递归:
递归调用这个动作在最后的递归函数叫做尾递归。scala编译器可以对尾递归做出重要优化,当其检测到尾递归就用新值更新函数参数,然后把它替换成一个回到函数开头的跳转。
你可以使用开关“-g:notailcalls”关掉编译器的尾递归优化。
别高兴太早,scala里尾递归优化的局限性很大,因为jvm指令集使实现更加先进的尾递归形式变得困难。尾递归优化限定了函数必须在最后一个操作调用本身,而不是转到某个“函数值”或什么其他的中间函数的情况。
在scala中,你不要刻意回避使用递归,相反,你应该尽量避免使用while和var配合实现的循环。
高阶函数:
带有其他函数作为参数的函数称为高阶函数。
柯里化:
柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。如下就是一个柯里化之后的函数:
def curriedSum(x: Int)(y: Int) = x + y
这里发生的事情是当你调用curriedSum时,实际上接连调用了两个传统函数。第一个调用的函数带单个名为x的参数,并返回第二个函数的函数值;这个被返回的函数带一个参数y,并返回最终计算结果。你可以使用部分应用函数表达式方式,来获取第一个调用返回的函数,也即第二个函数,如下:
val onePlus = curriedSum(3)_
高阶函数和柯里化配合使用可以提供灵活的抽象控制,更进一步,当函数只有一个参数时,在调用时,你可以使用花括号代替小括号,scala支持这种机制,其目的是让客户程序员写出包围在花括号内的函数字面量,从而让函数调用感觉更像抽象控制,不过需要注意的是:花括号也就是块表达式,因此你可以在其中填写多个表达式,但是最后一个表达式的值作为该块表达式的值并最终成为了函数参数。如果函数有两个以上的参数,那么你可以使用柯里化的方式来实现函数。
传名参数:
对于如下代码,myAssert带有一个函数参数,该参数变量的类型为不带函数参数的函数类型:
myAssert(predicate: () => Boolean) = {
if(!predicate())
throw new AssertionError
}
在使用时,我们需要使用如下的语法:
myAssert(() => 5 > 3)
这样很麻烦,我们可以使用如下称之为“传名参数”的语法简化之:
myAssert(predicate: => Boolean) = {
if(!predicate)
throw new AssertionError
}
以上代码在定义参数类型时是以“=>”开头而不是“() =>”,并在调用函数(通过函数类型的变量)时,不带“()”。现在你就可以这样使用了:
myAssert(5 > 3)
其中,“predicate: => Boolean”说明predicate是函数类型,在使用时传入的是函数字面量。注意与“predicate: Boolean”的不同,后者predicate是Boolean类型的(表达式)。
偏函数:
偏函数和部分应用函数是无关的。偏函数是只对函数定义域的一个子集进行定义的函数。 scala中用scala.PartialFunction[-T, +S]来表示。偏函数主要用于这样一种场景:对某些值现在还无法给出具体的操作(即需求还不明朗),也有可能存在几种处理方式(视乎具体的需求),我们可以先对需求明确的部分进行定义,以后可以再对定义域进行修改。PartialFunction中可以使用的方法如下:
isDefinedAt:判断定义域是否包含指定的输入。
orElse:补充对其他域的定义。
compose:组合其他函数形成一个新的函数,假设有两个函数f和g,那么表达式f _ compose g _则会形成一个f(g(x))形式的新函数。你可以使用该方法对定义域进行一定的偏移。
andThen:将两个相关的偏函数串接起来,调用顺序是先调用第一个函数,然后调用第二个,假设有两个函数f和g,那么表达式f _ andThen g _则会形成一个g(f(x))形式的新函数,刚好与compose相反。
每天向你推送最实用的干货,欢迎关注~
点击这里查看往期精彩内容:
以上是关于每天半小时掌握Scala(day 03)的主要内容,如果未能解决你的问题,请参考以下文章