Erlang中的if:赞美与非议

本文是考古Erlang语言中关于if语句的讨论。

语法

首先,我们该如何看待Erlang中的“if”语义呢?在Armstrong编写的《Erlang程序设计》一书中,很明确地将"case和if表达式"一起放在了同一节,并且先介绍了case,后介绍了if。在介绍if时,给出的语法例子如下:

if
 Guard1 ->
 Expr_seq1;
 Guard2 ->
 Expr_seq2;
 ...
end

可以从这个句式很明显地看出,整个if语句更像是一个case语句,通过一条条关卡(Guard)分割执行不同的子句。if语句会执行每一条关卡,如果结果为true则执行子表达式并结束,如果为false则依次向下匹配直到有一个关卡为true。

这里需要注意的一点是,整个语句必须保证至少有一个关卡为true,否则整段语句就会抛出异常。这样的语句在特定条件下会是一个异常错误:

if
 A > 0 ->
 do_this()
end

除非你有意想让错误发生。当然,一位熟悉编码的工程师知道,任何隐藏的意图与可能的失误混淆在一起时,整段代码都会变得难以阅读和理解,即使用注释明确标注,在之后的演进中也会难以迭代。推荐的做法以及各类项目中的实践是,在最后一个关卡中使用原子true,保证匹配最后一条子表达式。这就像Java中case最后的default一样。

if
 Guard1 ->
 Expr_seq1;
 Guard2 ->
 Expr_seq2;
 ...
 true ->
 Expr_default
end

决策表?

查看Erlang各类说明文档,stackoverflow中的例子,博客,都会优先推荐case,而非if。这里有一封归档邮件可以很清晰地说明if在开发者眼中的地位:http://erlang.org/pipermail/e... (也是Erlang编码规范中援引的说明用例)。

简单描述背景:Richard A. O'Keefe(直接搜索名字就可以找到这位发表了不少计算机语言学研究论文的Otago大学研究员)支持“聪明”地使用if语句。他给出的邮件标题就是“赞美Erlang中的if”。说明中列举了一篇论文,表明“结构化流程图优于伪代码”的观点,传统的伪代码类似:

IF GREEN
 THEN
 IF CRISPY
 THEN
 STEAM
 ELSE
 CHOP
 ENDIF
 ELSE
 FRY
 IF LEAFY
 THEN
 IF HARD
 THEN
 GRILL
 ELSE
 BOIL
 ENDIF
 ELSE
 BAKE
 ENDIF
ENDIF

常见的嵌套if引发的逻辑混乱。在Erlang中,通过巧妙地利用Erlang语法,可以把这一段逻辑变化为以下模式:

if Green, Crispy -> steam()
 ; Green, not Crispy -> chop()
 ; not Green, Leafy, Hard -> fry(), grill()
 ; not Green, Leafy, not Hard -> fry(), boil()
 ; not Green, not Leafy -> fry(), bake()
end

本质上是利用Erlang中的分号,逗号,空白符,创造出一张视觉上的“决策表”,能够清晰地表明每个分支对应的条件。

不过这种写法是不是让你的神经感受到了某种“奇技淫巧”,直觉上我们的代码中应该规避所有这一类写法取巧但难以理解/维护的代码,除非这段代码是至关重要的性能优化节点。而且,在编译器发展成熟的今天,即使是你认为的“性能优化”往往到了编译时会变得面目全非,也一定要经过性能测试才行,常常你做的这类优化根本无法比上编译器做的优化。

回归简朴

Anthony Ramine在回复邮件中首先就指出了这种写法奇怪的缩进给程序员带来的困扰。

其次,分号和逗号的混用在这种方式下难以被注意到,甚至写错了也难以被自动检测出来,例如他构建的以下例子(这里第三个分支的逗号改为了分号):

if Green, Crispy -> steam()
 ; Green, not Crispy -> chop()
 ; not Green; Leafy, Hard -> fry(), grill()
 ; not Green, Leafy, not Hard -> fry(), boil()
 ; not Green, not Leafy -> fry(), bake()
end

可以看到这类问题在Erlang开发中经常发生:https://github.com/rebar/reba...

对Erlang中if的评论甚至到了这种地步:https://stackoverflow.com/que...

"I have found that if you are relying on guards or case statements, you are probably 'doing it wrong' most of the time in Erlang."

为此,Anthony更希望去掉if语句中的关卡子句,甚至不再试用if子句。

顺带补充一下,在这个例子中,非绿色,没有叶子,也不坚硬的蔬菜将因为无法烹饪而报错。

编码规范

在我们参考的编码规范中,结合大家的开发经验,也提出了少用/不用if语句的要求。

改造代码中的if语句,我们可以用case(更容易和其它语言一样理解),而模式匹配是最好的选择:

-module(no_if).
 
-export([bad/1, better/1, good/1]).
 
bad(Connection) ->
 {Transport, Version} = other_place:get_http_params(),
 if
 Transport =/= cowboy_spdy, Version =:= 'HTTP/1.1' ->
 [{<<"connection">>, utils:atom_to_connection(Connection)}];
 true ->
 []
 end.
 
 
better(Connection) ->
 {Transport, Version} = other_place:get_http_params(),
 case {Transport, Version} of
 {cowboy_spdy, 'HTTP/1.1'} ->
 [{<<"connection">>, utils:atom_to_connection(Connection)}];
 {_, _} ->
 []
 end.
 
 
good(Connection) ->
 {Transport, Version} = other_place:get_http_params(),
 connection_headers(Transport, Version, Connection).
 
connection_headers(cowboy_spdy, 'HTTP/1.1', Connection) ->
 [{<<"connection">>, utils:atom_to_connection(Connection)}];
connection_headers(_, _, _) ->
 [].

参考资料

作者:Hotlink原文地址:https://segmentfault.com/a/1190000041729249

%s 个评论

要回复文章请先登录注册