Scala入门,以Java程序员的视角(一):函数

本人将结合《Scala学习手册》一书的内容,以Java程序员的视角为第一视角,用几篇小文来快速入门Scala。本篇内容为Scala函数。

函数编写形式

在Scala中编写函数和在Java中编写方法,形式上还是存在一些差别的。

普遍式

这种我称之为普遍式:

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

和Java不同,Scala类型的写法后置了。比如小括号内的: <type>表示行参的类型,中括号内的可选内容: <type>表示expression的返回类型;

这里的=类似Java 8里Lambda表达式的->

实例:

1
2
3
4
5
6
7
scala> def a1(i1: Int, i2: Int): Int = i1 + i2
a1: (i1: Int, i2: Int)Int
scala> a1(1, 2)
res1: Int = 3
scala>

大括号式

这种方式我称之为大括号式,因为采用{}

1
def <identifier>(<identifier>: <type>, ...)[[: <type>] = ]{}

实例:

1
2
3
4
scala> def b1(a: Int) { a + 1; val b = 1; print(s"sdsd$a") }
b1: (a: Int)Unit
scala>

讨论[: <type>] =存在与否的情况;

不存在[: <type>] = 的情况:

大括号内只可出现初始化声明,表达式以及语句;

这实际上是一个procedure,没有返回值的函数,通常并不推荐,因为它会“沉默”带返回值的表达式,例如:

1
2
3
4
scala> def b1(a: Int) {a + 1}
b1: (a: Int)Unit
scala>

可见虽然a + 1是一个有返回的表达式,但是返回值被忽略了;

存在[: <type>] = 的情况:

当添加了=后,就不是一个procedure了,大括号也可以单纯是{a}这种形式了,稍加更改前文中的实例再次运行:

1
2
3
4
5
scala> def b1(a: Int) = {a + 1}
b1: (a: Int)Int
scala> b1(2)
res2: Int = 3

调用函数后有输出结果:Int = 3;

无参式

这里指的是定义无参数输入的函数形式也存在两种,分别是无括号和有括号;

无括号:

1
2
3
4
5
scala> def c1 = "Ni Hao"
c1: String
scala> c1
res14: String = Ni Hao

注意c1:后直接是String,调用函数直接在REPL输入c1,此时不能输入c1();

有括号:

1
2
3
4
5
6
7
scala> def c2() = "ni hao"
c2: ()String
scala> c2
res15: String = ni hao
scala>

此次c2:后接的是()String,通过输入c2而非c2()来调用该函数也是可以的;

关于使用括号的约定:若函数有副作用,定义时应该加空括号,比如函数向控制台写数据等;

递归函数

本站文章《学习Java函数式编程(五):函数式思维与尾递归》已经提到了Java不支持尾递归优化,而Scala等JVM平台下的语言是通过将递归转化为循环来实现伪递归优化的。维基百科和相关文章都提到了,此方法不能优化交叉调用的尾递归,这里不深入说开去,毕竟本篇核心是入门Scala,但是并不难理解,文章末尾给出一些参考链接。

Scala提供了伪递归注解:

1
scala> @annotation.tailrec

该注解用来帮助程序员进行伪递归优化;

嵌套函数

Scala函数按照函数名以及其参数类型列表来区分彼此;

Scala支持嵌套函数,下面列举一例:

1
2
3
4
5
6
7
8
9
10
11
scala> def max(a: Int, b: Int, c: Int) = {
| def max(x: Int, y: Int) = if (x > y) x else y
| max(a, max(b, c))
| }
max: (a: Int, b: Int, c: Int)Int
scala> max(1, 2, 3)
res16: Int = 3
scala> max(10, 21, 3)
res17: Int = 21

函数的调用由内层层到外,所以即便内外函数名以及参数类型列表彼此相等,函数内也是优先调用内部函数;

为指定形参进行传参

也就是说,如果在传入参数的过程中,指定了要赋值给哪一个参数,那么就可以不按照原本的顺序传参,看下面的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
object Hello {
def main(args: Array[String]): Unit = {
print(max(b = 10,a = 21,c = 3))
}
def max(a: Int, b: Int, c: Int) = {
def max(x: Int, y: Int)= {
println(s"输入:$x, $y")
if (x > y) x else y
}
max(a, max(b, c))
}
}

输出为:

1
2
3
输入:10, 3
输入:21, 10
21

可见虽然传入的参数依次是10、21、3,但是由于指定了要赋值给哪一个参数,所以a、b、c分别为21、10、3;

Vararg参数

在Java中,该功能底层是通过数组实现的。Scala的书写与Java有差别,采用的是参数类型后面添加*,此后不能再跟非Varalg参数,下面用一个例子说明:

1
2
3
4
5
6
7
8
9
scala> def prin(nums: Int*) = {
| for (i <- nums) print(i + " ")
| }
prin: (nums: Int*)Unit
scala> prin(1, 2, 3, 4)
1 2 3 4
scala> prin(1, 5, 3, 4)
1 5 3 4

类型参数(Scala泛型)

这里要讨论的实际上就是Scala中的泛型,基本的格式如下:

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

Java中泛型的作用不必多言,如果不用泛型采用Any,最后进行类型转换凑合使用:

1
2
3
4
5
6
7
8
9
10
scala> def ide(a: Any): Any = a
ide: (a: Any)Any
scala> ide(3)
res24: Any = 3
scala> val s: String = ide("hello").toString
s: String = hello
scala>

采用泛型来重写ide函数:

1
2
3
4
scala> def ide[A](a: A): A = a
ide: [A](a: A)A
scala>

此时再调用ide函数将不需要显示转换类型:

1
2
3
4
5
6
7
scala> val ss = ide("Hello")
ss: String = Hello
scala> ss
res25: String = Hello
scala>

也可以写成val ss: String = ide[String]("Hello"),但是即便不写上类型,Scala可以通过类型推导得出正确的类型;

小结

“参数组”和“方法调用”因为和后续内容有关,本篇没有涉及。本篇主要探讨了Scala函数书写的形式和用法,每个小节都多少和Java相关内容进行了类比。

参考

知乎提问:Scala是如何实现尾递归优化的?

Java存在尾递归调用优化吗?

知乎提问:为什么 JVM 下的语言尾递归优化似乎都不完美?