文章目录

1.函数

函数是可重用逻辑的核心构件。

函数式编程语言特别强调支持创建高可用、可组合的函数,可以帮助开发人员利用函数组织代码。

在Scala中,函数(functions)是可重用的命名表达式。

如果遵循标准函数式编程方法论,尽可能构建纯(pure)函数,还会得到更大的好处。在函数式编程中,纯函数是指:

  • 有一个或多个输入参数
  • 只使用输入参数完成计算
  • 返回一个值
  • 对于相同的输入总返回相同的值
  • 不使用或影响函数之外的任何数据
  • 不受函数之外的任何数据的影响

纯函数基本上等价于数学中的函数,定义为仅由输入参数得出的一个计算式,这是函数式编程中的程序基本构件

  1. 定义无输入的函数

语法:

def <identifier> = <expression>

最基本的scala函数是表达式的一个命名包装器,如果需要一个函数来格式化当前数据,检查一个远程服务是否提供了新数据,或者只是要返回一个固定的值,就可以采用这个格式。

例子如下:

Scala学习记录——4.Scala中的函数_操作符

  1. 定义函数时指定返回类型

语法:

def <identifier> : <type> = <expression>

例子如下:

Scala学习记录——4.Scala中的函数_操作符_02

  1. 定义函数

语法:

def <identifier>( <identifier> : <type>[, ... ]) : <type> = <expression>

例子如下:

Scala学习记录——4.Scala中的函数_调用函数_03

有时需要提前退出函数,一个常见的用法是在出现不合法或异常的输入值时停止执行,例子如下:

Scala学习记录——4.Scala中的函数_Scala_04

2.过程

过程(procedure)是没有返回值的函数。以一个语句结尾的函数是一个过程。如果有一个简单的函数,没有显示的返回类型,而且最后是一个语句,Scala编译器就会推导出这个函数的返回类型为Unit,这表示没有值。对于超过一行的过程,可以显示地指定类型Unit,这会让读者清楚地知道这个函数(过程)没有返回值。

示例如下:

Scala学习记录——4.Scala中的函数_操作符_05

3.用空括号定义函数

要定义和调用一个无输入的函数(即没有输入参数的函数),还有一种方法:可以使用空括号。

语法:

def <identifier> ()[: <type> ]= <expression>

例子如下:

Scala学习记录——4.Scala中的函数_scala_06

注意:如果定义一个函数时没有加小括号,Scala就不允许在调用这个函数时增加小括号。这个规则可以避免将调用无括号的函数与调用函数返回值相混淆

另外,Scala中有一个规定:如果函数有副作用(也就是说,会修改其范围之外的数据),定义时就应当加空括号。例如,如果一个无输入的函数要向控制台写一个消息,定义时就应当加空括号。

4.使用表达式块调用函数

使用一个参数调用函数时,可以利用一个用大括号包围的表达式块发送参数,而不是用小括号包围值。通过使用表达式块调用函数,可以完成一些计算和其他动作,然后利用这个块的返回值调用函数。

语法:

<function identifier><expression block>

示例如下:

Scala学习记录——4.Scala中的函数_操作符_07


如果已经计算出希望传入函数的值,很自然地,可以使用小括号指定函数参数。不过,如果计算式只是用于这个函数,而且可以保证代码的可读性,用表达式块调用函数将是一个很好的选择。

5.递归函数

递归(recursive)函数就是调用自身的函数,可能要检查某类参数或外部条件来避免函数调用进入无限循环。

例子如下:

Scala学习记录——4.Scala中的函数_操作符_08

使用递归函数的一个问题是可能会遇到致命的“栈溢出”错误,这表示调用一个递归函数的次数太多,耗尽了所有已分配的栈空间。

为了避免这种情况,Scala编译器可以用尾递归(tail-recursion)优化一些递归函数,使得递归调用不使用额外的栈空间。对于利用尾递归优化的函数,递归调用不会创建新的栈空间,而是使用当前函数的栈控件。只有最后一个语句是递归调用的函数才能由Scala编译器完成尾递归优化。如果调用函数本身的结果不作为直接返回值,而是有其他用途,这个函数就不能优化。

幸运的是,可以利用函数注解(function annotation)来标志一个函数将完成递归优化。函数注解是从Java编程语言沿用而来的一种特殊的语法,在函数定义前加一个@符号和一个注解类型来标志它有特殊用途。如果用尾递归函数注解标志一个函数,而它并不能完成尾递归优化,就会在编译时导致一个错误。

要标志一个函数将完成尾递归优化,需要在函数定义前或者在前一行上增加文本@annotation.tailrec

例子如下:

Scala学习记录——4.Scala中的函数_调用函数_09

因为递归调用不是这个函数的最后一个语句,所以这个函数不能优化。下面把“if"和”else"条件调换一下,例子如下:

Scala学习记录——4.Scala中的函数_操作符_10

表面上看递归调用确实是函数的最后一项。不过,可以看到,得到递归调用的结果后,将它乘以一个值,所以实际上这个乘法才是函数的最后一个语句,而不是递归调用。

要修正这个问题,可以把乘法移到被调用函数的最前面,而不要乘以函数调用的结果,示例如下:

Scala学习记录——4.Scala中的函数_scala_11

6.嵌套函数

函数是命名的参数化表达式块,而表达式块是可以嵌套的,所以函数本身也是可以嵌套的。

示例如下:

Scala学习记录——4.Scala中的函数_scala_12

注意:这里的嵌套函数与外部函数同名。不过,由于它们的参数不同,所以它们之间不会发生冲突。Scala函数按函数名以及其参数类型列表来区分。不过,即使函数名和参数列表相同,它们也不会冲突,因为局部函数优先于外部函数。

7.用命名参数调用函数

调用函数的惯例是按原先定义时的顺序指定参数。不过,在Scala中,还可以按名调用参数,这样就允许不按顺序指定参数。

语法:

<fuunction name>(<parameter> = <value>)

例子如下:

Scala学习记录——4.Scala中的函数_调用函数_13

8.有默认值的参数

类似于Java的函数重载。Scala为这个问题提供了一个更简洁的解决方案:可以为任意参数指定默认值,使得调用者可以忽略这个参数。

语法:

def <identifier>(<identifier> : <type> = <value>) : <type>

示例如下:

Scala学习记录——4.Scala中的函数_Scala_14

可以重新组织这个函数,让必要的参数在前,这样就可以直接调用这个函数而不再需要使用参数名,示例如下:

Scala学习记录——4.Scala中的函数_调用函数_15

从编程风格来讲,最好适当地组织函数参数,让必要参数在前,而有默认值的参数在后。

9.Vararg参数

Vararg参数是一个函数参数,可以匹配调用者的0个或多个参数,类似于Java中的可变长参数。

Scala也支持vararg参数,所以可以定义输入参数个数可变的函数。vararg参数后面不能跟非vararg,因为无法加以区分。

要标志一个参数匹配一个或多个输入实参,在函数定义中需要该参数类型后面增加一个星号(*)。

示例如下:

Scala学习记录——4.Scala中的函数_Scala_16

10.参数组

到目前为止,我们已经了解可以对函数定义参数化,并用小括号包围参数表。Scala还提供了另外一种选择,可以把参数表分解为参数组(parameter groups),每个参数组分别用小括号分隔。

示例如下:

Scala学习记录——4.Scala中的函数_调用函数_17

在这个例子中,参数组看起来没有太大意义,只有对函数字面量使用参数组时才会真正体验出参数组的好处。

11.类型参数

在Scala中,作为值参数的补充,还可以传递类型参数,类型参数指示了值参数或返回值使用的类型。通过使用类型参数,可以提高函数的灵活性和可重用性,这样一来,函数参数或返回值的类型不再固定,而是可以由函数调用者设置。

语法:

def <function-name>[type-name](parameter-name> : <type-name>) : <type-name> ...

先看一个例子:

Scala学习记录——4.Scala中的函数_操作符_18

这里定义了一个函数,参数类型和返回类型都是Any,但是将String传入进去时,出现报错。因为函数的返回类型是Any,而结果却赋给了一个String,所以导致了Scala编译错误。

怎么解决这个问题?不必为使用特定的类型(如String或Int)来定义函数,也不要使用一个通用的“跟类型”(如Any),可以将类型参数化,使它适用于调用者想要使用的任何类型。

下面是使用类型参数定义的同一性函数,这样它就可以用于你提供的任何类型了,示例如下:

Scala学习记录——4.Scala中的函数_调用函数_19

其实就类似于Java中的泛型

由于Scala之中的特性——“类型推导”,下面把前例中的两个函数调用中的类型参数删除,示例如下:

Scala学习记录——4.Scala中的函数_调用函数_20


这里只有一个可以删除的显式类型,即值的类型。

甚至可以进一步的删除,示例如下:

Scala学习记录——4.Scala中的函数_操作符_21

这里可以看到类型参数和类型推导的意义:向一个函数传入的字面量就足以改变它的值参数类型、返回值类型,以及将赋为返回值的值的类型

一般来说,这并不是定义值的最可读的做法,因为读代码的人可能需要仔细检查函数定义才能确定函数返回值将赋给什么类型的值。不过,这确实很快地展示了Scala类型系统的灵活性和强大功能,从中也可以看出它对高可重用函数的支持。

12.方法和操作符

在实际中,函数常存在于对象中,用来处理对象的数据,所以对函数更适合的说法通常是“方法”。

方法(method)是类中定义的一个函数,这个类的所有实例都会有这个方法。Scala中调用方法的标准做法是使用中缀点记法(infix dot notation),方法名前面有实例名和一个点(.)分割符作为前缀。

语法:

<class instance>.<method>[(<parameters>)]

String类型中提供了很多有用的方法,示例如下:

Scala学习记录——4.Scala中的函数_调用函数_22

  • String.endsWith(“a”):检测字符串后缀是否存在a字符,返回的是Boolean类型

你会发现,Scala中的大部分类型都提供了丰富的方法可以使用。要想成为一个熟练的Scala开发人员,建议阅读Scala API文档(https://docs.scala-lang.org/api/all.html)提供了可用类型及其方法的一个完整列表。

下面继续研究新方法,这里将尝试使用Double类型的一些方法,示例如下:

Scala学习记录——4.Scala中的函数_操作符_23

  • Double.round:遵循四舍五入把原值转化为指定小数位数
  • Double.floor:向下舍入为指定小数位数
  • Double.compare(“x”):将数字与x进行比较,小于、等于、大于对应的返回值为-1、0、1
  • Double.+(“x”):返回数字与x相加的取值,是加法操作符的具体实现

事实上,Scala中实际上没有加法操作符,也没有任何其他算术运算符。我们在Scala中使用的所有算术运算符其实都是方法,写为简单的函数,它们使用相应的操作符符号作为函数名,并绑定到一个特定的类型。

之所以可以这么做,是因为还可以采用另一种形式调用对象的方法,这称为操作符记法(operator notation),这里不使用传统的点记法,而是使用空格来分割对象、操作符方法和方法的参数(只有一个参数)。每次写 2 + 3 时,Scala编译器会把它识别为操作符记法,并相应地处理,就好像写为2.+(3)一样,这里调用了值为2的一个Int的加法方法,并提供参数3,最后会返回值5。

要采用操作符记法调用对象的方法,要求这个方法只有一个参数,另外对象、方法和这个参数之间要用空格分隔。不需要其他的任何标点符号。

语法:

<object> <method> <parameter>

对于这种记法,更准确的说法应当是中缀操作符记法,因为操作符(对象的方法)位于两个操作数中间。

使用操作符记法重写前面两个例子的调用,示例如下:

Scala学习记录——4.Scala中的函数_调用函数_24

注意:类似简单数学运算,操作符记法只适用于单参数方法,不过也可以用于包含多个参数的方法。要做到这一点,需要把参数表包围在小括号里,把它处理为多个(但包装的)参数,例如,可以采用“staring" substring (1,4)的形式调用String.substring(start,end)

要确保在清晰可读的前提下才使用操作符记法,你会发现经常要用到这种记法。

13.编写可读的函数

编写函数的目的就是为了重用(否则,完全可以写为一次性的表达式)。而确保函数可重用性的最好办法就是保证函数对其他开发人员可读。可读的函数清晰明了,便于理解,而且简单。

确保函数可读有两种方法:

  1. 尽量保证函数简短,命名适当而且含义明确。把复杂的函数分解为更简单的函数,应当不超过标准的一页纸高度(例如,40行),这样读者就不用上下滚动才能查看整个函数。另外要使用能充分体现函数作用的名字,能清楚地看出这个函数要完成什么工作。如果做到了这两点,对于开发人员来说,就能很清楚地了解你的函数的目的和实现。
  2. 在适当的地方增加注释。Scala支持的注释语法与Java和C++相同:
  1. 单行注释:双斜线(//)
  2. 范围注释:斜线加星号(/* … */)
  3. Scaladoc工具注释:斜线加两个星号(/** … **/)。参数可以用一个@param关键字指示,后面是参数名及其描述。Scaladoc首部是函数注释的一个标准格式。

14.练习

  1. 写一个函数,给定一个圆的半径,计算这个圆的面积

Scala学习记录——4.Scala中的函数_调用函数_25

  1. 写一个递归函数,可以5、10、15…地打印从5到50的值,但不要使用for或while循环,可以实现尾递归吗?

Scala学习记录——4.Scala中的函数_scala_26

  1. 写一个函数,取一个毫秒值,返回一个字符串,按天、小时、分和秒描述这个值。输入值的最佳类型是什么?
    作者说:由于时间转换关系复杂,这里仅给出思路

Scala学习记录——4.Scala中的函数_调用函数_27

  1. 写一个函数,计算第一个值以第二个值为指数的幂。首先试着用math.pow来写这个函数,然后用你自己的算式来实现。你使用变量了吗?有没有方法只使用不可变的数据?你选择一个足够大的数值类型了吗?

Scala学习记录——4.Scala中的函数_操作符_28


后三题没有思路…希望有大佬可以在留言区指点一下

  1. 写一个函数,计算一对2D点(x和y)之差,并把结果返回为一个点。提示:可以使用元组
  2. 写一个函数,取一个大小为2的元组,返回第一个位置上的Int值(如果有)。提示:可以使用类型参数和isInstanceOf类型操作
  3. 写一个函数,取一个大小为3的元组,返回一个大小为6的元组,原来的各个参数后面跟着相应的String表示。例如,用(true,22,25,“yes”)调用这个函数会返回(true,“true”,22.5,“22.5”,“yes",“yes”),能不能保证所有可能类型的元组都与你的函数兼容?调用这个函数时,能不能保证所有可能类型的元组都与你的函数兼容?调用这个函数时,能不能使用显示类型做到这一点(除了对函数结果使用显式类型,用来存储结果的值也使用显式类型?)