0%

Scala 函数解析

1. 代换模型

Scala 使用代换模型对函数和表达式进行解析工作;

所谓的代换模型就是类似平常算术的过程;

从左到右地将函数和表达式一步一步转换,最终转换成值。

例如:

1
(2 * 2) + (4 * 5)

对于上面的式子 Scala 是如何解析的呢?

我们从左到右地解析,首先,我们解析 (2 * 2) 的内容,将其替换成值 4

此时,式子变为:

1
4 + (4 * 5)

由于有括号,和乘法的算术等级较高,所以我们接下来对 (4 * 5) 进行解析;

此时,式子变为:

1
4 + 20

此时,我们计算上面的值,最后得到 24。

可以看到,Scala 的解析是符合我们通常的算术解析规律的。

2. 代换模型的缺陷

使用代换模型最重要的一个要求就是,我们的表达式最终 能够 规约到一个

如果它最终不能够规约到一个值(无限循环,Non-Terminate);

或者表达式对其外部的变量产生了影响(副作用);

都会对代换模型造成污染。

3. 副作用

所谓的副作用就是指的是,函数和表达式的执行过程修改了外部的变量。

例如,c++ 这个表达式就具有很明显的副作用;

因为我们不能够直接将这个表达式规约为一个值;

在执行的过程中,我们需要对 c 这个外部传入的变量进行修改;

这就让这个表达式显得不够纯粹,这时候我们就说它具有 副作用

4. 不能终结的解析

所谓的不能终结的解析就是指的一个函数返回它自身;

例如:

1
def loop(x: Int) = loop

这个函数的解析永远也不会完成,这是因为我们如果使用代换模型对其进行解析的话,会发现,它的解析结果一直是其自身;

所以,对它的解析会一直进行下去,无法完成。

5. 参数解析

Scala 有两种参数解析方式,不像其他的指令性语言只有一种解析方式;

其中的一种叫传值调用(call-by-value),另一种叫传名调用(call-by-name)。

5.1 传值调用(call-by-value)

这是 Scala 的默认的参数解析方式,也是其他指令性语言常用的参数解析方式。

主要的解析步骤如下:

  1. 将传入参数的表达式解析为值
  2. 将函数使用函数体进行替换
  3. 将函数的形参替换为第一步中得到的实参

例如:

1
2
3
def square(x: Int) = x * x

square(2 + 2)

对于上面的代码,解析步骤如下:

  1. 2 + 2 进行计算,得到它的值 4
  2. square 使用它的函数体进行替换,得到 x * x
  3. x 代换为 4
  4. 计算出结果 16

5.2 传名调用(call-by-name)

这是 Scala 的另一种参数解析方式,也是其他指令性语言不具备的。

只要在定义参数时,使用 => 就可以定义传名调用的参数

1
def square(x: => Int) = x * x

主要的解析步骤如下:

  1. 将函数名替换为函数体
  2. 直接将参数的表达式代入形参
  3. 对得到的表达式进行解析和计算,得出结果

还是使用上面的例子,解析步骤如下:

  1. square 替换为 x * x
  2. 2 + 2 代入 x
  3. 对得到的式子 (2 + 2) * (2 + 2) 进行代换模型的计算
  4. 得到结果 16

5.3 区别

那么这两种计算结果有什么区别呢?

首先,传名调用具有懒加载的功能,直到参数 被使用 的时候,才进行参数表达式的解析;

例如:

1
2
3
4
5
def first(x: Int, y: Int) = x

def first(x: => Int, y: => Int) = x

first(2 * 2, 4 * 4)

此时,我们忽略了第二个参数,对于传名调用来说,它不需要解析 y 这个参数,只需要将它传入函数体即可;

而对于传值调用,则需要先解析出 xy 的值,即使 y 的值不会在函数体内使用到。

其次,传名调用可以避免无限循环问题;

对于上面的例子来说,我们可以这么调用:

1
first(1, loop)

对于传名调用来说,由于它是直接传入函数体,然后,函数体并没有使用 y 这个参数;

此时,我们就避免了对 loop 的解析工作;

但是对于传值调用则不然,我们还是要对 loop 进行解析,从而出现无限循环问题。

5.4 默认传值调用的原因

既然传名调用具有那么多的好处,但是为什么还要默认使用传值调用呢?

这是因为传值调用在具体实践过程中,比传名调用的执行要快;

其次,由于 Scala 并不是纯函数式语言,实际上,它的函数还是允许有副作用的;

并且同时还要支持和 Java 的互调用,而 Java 是指令式语言,采用传值调用显然会更好些。