关于变量绑定的棘手问题

在我的工作中,我有个任务是为Erlang程序员应聘者设计一套选择题考卷,我决定包含一个关于变量绑定的棘手问题。在加入这个问题之前,我决定自己先尝试一下(你懂的……我这是为了预防万一)并且我发现了一些可能让你第一眼看到觉得很惊讶的事情,尽管这在事后是很明显的。

问题

不用说,这个问题最后没有列入考试,所以不要指望从这篇文章中得到一个简单的答案。这个问题是:

假设一开始没有任何变量被绑定,那么下面的表达式计算结束后,哪些变量被绑定了?

1
2
3
4
5
6
A = 1,
B = case rand:uniform() of
C when C < 0.5 -> C;
D when D >= 0.5 -> 1 - D
end,
A + B.

选项:

  1. 没有
  2. A 和 B
  3. A、B、C 和 D
  4. 不可能确切地知道

请不要先往下阅读,而是停在这里!你自己先思考答案是什么?而且不能启动一个Erlang shell来测试这条表达式。切接不要偷看下面的内容!

答案

让我们先剔除那些显而易见的选项。

答案不可能是选项1,因为我们在表达式里明确地绑定了一个变量。选项1唯一能被选上的条件是表达式发生错误。毫无疑问,这不可能发生。

选项3也不正确,因为代码执行路径有两个分支,其中一个不会绑定C变量,另一个不会绑定D变量。

在我看来,答案是选项2。我认为,C 和 D只是case表达式里的临时变量。

但是,情况并非如此。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1> A = 1,
1> B = case rand:uniform() of
1> C when C < 0.5 -> C;
1> D when D >= 0.5 -> 1 - D
1> end,
1> A + B.
1.063499807059608
2> b().
A = 1
B = 0.063499807059608
C = 0.063499807059608
ok
3> f().
ok
4> e(1).
1.3765011035828076
5> b().
A = 1
B = 0.37650110358280764
C = 0.37650110358280764
ok
6> f().
ok
7> e(1).
1.0869922372383418
8> b().
A = 1
B = 0.08699223723834182
D = 0.9130077627616582
ok
9>

如你所见,有时候C被绑定,有时候却是D被绑定,这依赖于case语句所执行的分支,即使被执行的case语句分支的表达式不需要这些变量,也会发生这样的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
10> case rand:uniform() of
10> C when C < 0.5 -> low;
10> D when D >= 0.5 -> high
10> end.
low
11> b().
C = 0.3006547812129776
ok
12> f(), e(10), b().
C = 0.12584026085863464
ok
13> f(), e(10), b().
D = 0.9295007071083405
ok
14>

到底是为什么呢?

正如我之前说过的,这个问题可能令人惊讶,但事实上,它是非常明显的,而且有据可查。case语句分支的头部匹配变量是完全有效的,它可以像下面这样使用:

1
2
3
4
5
B = case rand:uniform() of
C when C < 0.5 -> low;
C when C >= 0.5 -> high
end,
{B, C}.

这是一种非常复杂的编码方式,我根本不推荐这样做,你甚至可以把它弄得更糟糕:

1
2
3
4
5
case rand:uniform() of
C when C < 0.5 -> B = low;
C when C >= 0.5 -> B = high
end,
{B, C}.

但是,除了代码的样式和可维护性问题之外,这段代码是完全有效的,这意味着在case表达式的被计算之后,在case子句中(包括在头部和身体中)被绑定的变量都是绑定的。我知道有更学术的方式来表达这个问题,比如用像闭包或作用域等词汇,但是我将把这个解释权留给像iraunkortasuna这样的大牛们,他们比我能干的多。

考虑到这一点,在我们最初的例子中,无论是C还是D(但不是两者)都是在对整个表达式的计算之后绑定的,这是合理的。但是,你想知道,这样的代码是非常不安全的……难道Erlang不应该警告我吗?Erlang不应该阻止我写出这样一个非确定性的东西吗?

当然,Erlang会帮我们的!不过不是Erlang的shell,而是Erlang的编译器,它会对这类代码进行告警,而且它非常聪明。如下面的例子:

1
2
3
4
5
6
7
8
-module(x).
-export([bad/1]).
bad(A) ->
B = case rand:uniform() of
C when C < 0.5 -> C;
D when D >= 0.5 -> 1 - D
end,
A + B.

这是我们的原始代码,在这种情况下,即使C或D在case语句之后是未绑定的,因为它们没有被使用,编译器什么也不说。

现在我们看看另一个版本的代码:

1
2
3
4
5
6
7
8
-module(x).
-export([bad/1]).
bad(A) ->
B = case rand:uniform() of
C when C < 0.5 -> C;
D when D >= 0.5 -> 1 - D
end,
A + B - C.

在这个例子里,编译器将不会编译这个模块,相反,它会报告如下错误:

1
x.erl:8: variable 'C' unsafe in 'case' (line 4)

我们可以对上述错误消息的可用性和可读性进行深入的讨论,但是这个模块不能编译的事实毫无疑问对Erlang开发者是有帮助的。

小贴士:如果在编译Erlang模块时看到类似这样的错误,请注意警告:8(其中使用了不安全变量)和4(变量可能绑定的地方)中的两个行号。

*原文链接https://medium.com/erlang-battleground/tricky-question-25a956298b9d