Scala 函数解析
1. 代换模型
Scala 使用代换模型对函数和表达式进行解析工作;
所谓的代换模型就是类似平常算术的过程;
从左到右地将函数和表达式一步一步转换,最终转换成值。
例如:
(2 * 2) + (4 * 5)
对于上面的式子 Scala 是如何解析的呢?
我们从左到右地解析,首先,我们解析 (2 * 2)
的内容,将其替换成值 4
;
此时,式子变为:
4 + (4 * 5)
由于有括号,和乘法的算术等级较高,所以我们接下来对 (4 * 5)
进行解析;
此时,式子变为:
4 + 20
此时,我们计算上面的值,最后得到 24。
可以看到,Scala 的解析是符合我们通常的算术解析规律的。
2. 代换模型的缺陷
使用代换模型最重要的一个要求就是,我们的表达式最终 能够 规约到一个 值;
如果它最终不能够规约到一个值(无限循环,Non-Terminate);
或者表达式对其外部的变量产生了影响(副作用);
都会对代换模型造成污染。
3. 副作用
所谓的副作用就是指的是,函数和表达式的执行过程修改了外部的变量。
例如,c++
这个表达式就具有很明显的副作用;
因为我们不能够直接将这个表达式规约为一个值;
在执行的过程中,我们需要对 c
这个外部传入的变量进行修改;
这就让这个表达式显得不够纯粹,这时候我们就说它具有 副作用
4. 不能终结的解析
所谓的不能终结的解析就是指的一个函数返回它自身;
例如:
def loop(x: Int) = loop
这个函数的解析永远也不会完成,这是因为我们如果使用代换模型对其进行解析的话,会发现,它的解析结果一直是其自身;
所以,对它的解析会一直进行下去,无法完成。
5. 参数解析
Scala 有两种参数解析方式,不像其他的指令性语言只有一种解析方式;
其中的一种叫传值调用(call-by-value),另一种叫传名调用(call-by-name)。
5.1 传值调用(call-by-value)
这是 Scala 的默认的参数解析方式,也是其他指令性语言常用的参数解析方式。
主要的解析步骤如下:
- 将传入参数的表达式解析为值
- 将函数使用函数体进行替换
- 将函数的形参替换为第一步中得到的实参
例如:
def square(x: Int) = x * x
square(2 + 2)
对于上面的代码,解析步骤如下:
- 将
2 + 2
进行计算,得到它的值4
- 将
square
使用它的函数体进行替换,得到x * x
- 将
x
代换为4
- 计算出结果
16
5.2 传名调用(call-by-name)
这是 Scala 的另一种参数解析方式,也是其他指令性语言不具备的。
只要在定义参数时,使用 =>
就可以定义传名调用的参数
def square(x: => Int) = x * x
主要的解析步骤如下:
- 将函数名替换为函数体
- 直接将参数的表达式代入形参
- 对得到的表达式进行解析和计算,得出结果
还是使用上面的例子,解析步骤如下:
- 将
square
替换为x * x
- 将
2 + 2
代入x
中 - 对得到的式子
(2 + 2) * (2 + 2)
进行代换模型的计算 - 得到结果
16
5.3 区别
那么这两种计算结果有什么区别呢?
首先,传名调用具有懒加载的功能,直到参数 被使用 的时候,才进行参数表达式的解析;
例如:
def first(x: Int, y: Int) = x
def first(x: => Int, y: => Int) = x
first(2 * 2, 4 * 4)
此时,我们忽略了第二个参数,对于传名调用来说,它不需要解析 y
这个参数,只需要将它传入函数体即可;
而对于传值调用,则需要先解析出 x
和 y
的值,即使 y
的值不会在函数体内使用到。
其次,传名调用可以避免无限循环问题;
对于上面的例子来说,我们可以这么调用:
first(1, loop)
对于传名调用来说,由于它是直接传入函数体,然后,函数体并没有使用 y
这个参数;
此时,我们就避免了对 loop
的解析工作;
但是对于传值调用则不然,我们还是要对 loop
进行解析,从而出现无限循环问题。
5.4 默认传值调用的原因
既然传名调用具有那么多的好处,但是为什么还要默认使用传值调用呢?
这是因为传值调用在具体实践过程中,比传名调用的执行要快;
其次,由于 Scala 并不是纯函数式语言,实际上,它的函数还是允许有副作用的;
并且同时还要支持和 Java 的互调用,而 Java 是指令式语言,采用传值调用显然会更好些。