Core Erlang 示例

2018年5月7日 · 作者:Björn Gustavsson

这篇博文是关于 Core Erlang 格式的第一篇。在这篇博文中,我们将通过比较 Erlang 代码和相应的 Core Erlang 代码的示例来介绍 Core Erlang 格式。

我使用了以下命令将我的示例模块转换为 Core Erlang 代码

$ erlc +time +to_core core_example.erl
Compiling "core_example"
 parse_module                  :      0.000 s      10.8 kB
 transform_module              :      0.000 s      10.8 kB
 lint_module                   :      0.003 s      10.8 kB
 expand_records                :      0.000 s      10.8 kB
 core                          :      0.000 s      89.9 kB
 sys_core_fold                 :      0.000 s      58.6 kB
 core_transforms               :      0.000 s      58.6 kB
 listing                       :      0.002 s      58.6 kB

上一篇博客文章探讨了从 parse_moduleexpand_records 的过程。core 过程将抽象代码转换为 Core Erlang 代码。我们将在未来的博文中更多地讨论 Core Erlang 过程。

我稍微编辑了这些示例,使它们更容易阅读。在本博文的最后将有一个未编辑的示例。

有很多内容要讲,所以我们开始吧!

最简单的函数 #

让我们从最简单的函数开始,一个没有参数并返回原子值的函数

simplest() -> 'ok'.

在 Core Erlang 中,它将是

'simplest'/0 =
    fun () ->
	'ok'

从这个例子中,我们可以总结出以下原则

  • 原子值总是被引用。

  • 函数的命名已与函数的实现分离。

  • fun 的主体后面没有像 Erlang 中那样的 end

稍微不那么简单 #

这是一个稍微复杂一些的函数

id(I) -> I.

在 Core Erlang 中

'id'/1 =
    fun (_@c0) ->
	_@c0

注意:所有示例均使用 OTP 20 编译。在即将到来的 OTP 21 中,生成的变量名称将有所不同。

本质上,变量的命名方式与 Erlang 中相同。在转换为 Core Erlang 时,编译器会为函数头中的参数生成新的变量名称。以下代码也是有效的 Core Erlang 代码

'id'/1 =
    fun (I) ->
	I

多个子句 #

这是一个具有多个子句的函数

a(42) -> ok;
a(_) -> error.

在 Core Erlang 中

'a'/1 =
    fun (_@c0) ->
	case _@c0 of
	  <42> when 'true' ->
	      'ok'
	  <_@c2> when 'true' ->
	      'error'
	end
  • 一个 fun 只能有一个子句。

  • 模式匹配必须在 case 中完成,而不是在 fun 头中。

  • case 中的每个子句都必须有 guard。

  • _ 在 Core Erlang 中不是有效的变量名。不感兴趣的值必须绑定到新变量。

  • 模式周围的 <> 将很快解释。

在 Erlang 中,也可以使用 case 来编写多个函数子句,如下所示

b(N) ->
    case N of
        42 -> ok;
        _ -> error
    end.

Core Erlang 代码与 a/1 的 Core Erlang 代码基本相同

'b'/1 =
    fun (_@c0) ->
	case _@c0 of
	  <42> when 'true' ->
	      'ok'
	  <_@c3> when 'true' ->
	      'error'
	end

两个子句,三个参数 #

让我们尝试多个参数

c(inc, Base, N) ->
    Base+N;
c(_, Base, _) ->
    Base.

在 Core Erlang 中

'c'/3 =
    fun (_@c2,_@c1,_@c0) ->
	case <_@c2,_@c1,_@c0> of
	  <'inc',Base,N> when 'true' ->
	      call 'erlang':'+'(Base, N)
	  <_@c6,Base,_@c7> when 'true' ->
	      Base
	end
  • <> 表示一个值列表case 中每个子句的模式始终是值列表的一部分。case 表达式是一个值列表,除非只有一个表达式。

  • 诸如 + 之类的运算符不是 Core Erlang 语言的一部分,因此编译器已将 + 的使用转换为对 BIF erlang:'+'/2 的调用。

If #

让我们看看 if 是如何实现的

d(A, B) ->
    if
        A > B ->
            greater;
        true ->
            not_greater
    end.

在 Core Erlang 中

'd'/2 =
    fun (_@c1,_@c0) ->
	case <> of
	  <> when call 'erlang':'>'(_@c1, _@c0) ->
	      'greater'
	  <> when 'true' ->
	      'not_greater'
	end
  • case 表达式和模式都是具有零个元素的值列表。所有的操作都在 guard 中。

重复变量 #

在 Erlang 中,一个变量可以在一个子句或一个模式中重复出现,以表明这些值必须相同

cmp(Same, Same) -> same;
cmp(_, _) -> different.

Core Erlang 不允许重复变量

'cmp'/2 =
    fun (_@c1,_@c0) ->
	case <_@c1,_@c0> of
	  <Same,_@c4> when call 'erlang':'=:='(_@c4, Same) ->
	      'same'
	  <_@c5,_@c6> when 'true' ->
	      'different'
	end
  • 在这里,变量 Same 的第二次出现已重命名为名为 _@c4 的新变量,并添加了一个 guard 来比较 Same_@c4

异常 #

如果使用除 42 之外的任何其他值调用此函数,则该函数将失败并抛出 function_clause 异常

e(42) -> ok.

在 Core Erlang 中

'e'/1 =
    fun (_@c0) ->
	case _@c0 of
	  <42> when 'true' ->
	      'ok'
	  <_@c1> when 'true' ->
	      primop 'match_fail'({'function_clause',_@c1})
	end
  • Core Erlang 中的 case 不能在末尾失败,也就是说,必须始终有一个子句可以匹配。

  • 在此示例中,最后一个带有变量模式和 true guard 的子句保证匹配。

  • 最后一个子句的主体调用一个 primop 来生成函数子句异常。Primop 是 Erlang 实现提供的原始操作,但未在 Core Erlang 语言规范中指定。

这是一个类似的函数,只不过它使用了 case,因此,如果使用除 42 之外的任何其他参数调用它,它将生成 case_clause 异常

f(N) ->
    case N of
        42 -> ok
    end.

Core Erlang 代码与 e/1 的代码类似

'f'/1 =
    fun (_@c0) ->
	case _@c0 of
	  <42> when 'true' ->
	      'ok'
	  <_@c1> when 'true' ->
	      primop 'match_fail'({'case_clause',_@c1})
	end
  • 唯一的区别是 match_fail primop 的参数。

让我们再次重写此函数

g(N) ->
    42 = N,
    ok.

在 Core Erlang 中

'g'/1 =
    fun (_@c0) ->
	case _@c0 of
	  <42> when 'true' ->
	      'ok'
	  <_@c1> when 'true' ->
	      primop 'match_fail'({'badmatch',_@c1})
	end
  • 同样,唯一的区别是 match_fail primop 的参数。

使用 ‘let’ 绑定变量 #

这是一个绑定变量 I 的函数

h(A) ->
    I = id(A),
    I + A.

在 Core Erlang 中

'h'/1 =
    fun (_@c0) ->
	let <I> =
              apply 'id'/1(_@c0)
	in
              call 'erlang':'+'(I, _@c0)
  • apply 调用一个 fun 或本地函数。

  • apply 的返回值绑定到变量 I

  • 变量 I 只能在 in 关键字后面的代码中使用。

  • 变量名在值列表中。这是因为 let 可以一次绑定多个变量。

在 ‘let’ 中绑定多个变量 #

Erlang 本质上没有作用域。绑定变量后,该变量将保持绑定到函数末尾。例如,在 case 中绑定的变量可以在 case 之后使用

i(E) ->
    case E of
        a ->
            X = 1,
            Y = 10;
        b ->
            X = 23,
            Y = 17
    end,
    {X,Y}.

在 Core Erlang 中

'i'/1 =
    fun (_@c0) ->
	let <_@c7,X,Y> =
	    case _@c0 of
	      <'a'> when 'true' ->
		  <10,1,10>
	      <'b'> when 'true' ->
		  <17,23,17>
	      <_@c5> when 'true' ->
		  primop 'match_fail'({'case_clause',_@c5})
	    end
	in
	    {X,Y}
  • Core Erlang 中的 case 不导出任何变量。要在 case 之后使用的所有变量都必须显式返回。

  • 在此示例中,case 的前两个子句返回一个包含三个值的值列表。第一个值是 case 的返回值,在这种情况下会被忽略。其他两个值分别是分配给 XY 变量的值。

  • case 返回的值在 let 中绑定。忽略的返回值绑定到一个新变量(_@c7),该变量永远不会被使用。导出的值绑定到 XY 变量。

未编辑的 Core Erlang 代码 #

到目前为止,所有 Core Erlang 示例都经过编辑,以使我试图表达的观点更加突出。让我们看一下先前示例的未编辑版本

'e'/1 =
    %% Line 33
    fun (_@c0) ->
	case _@c0 of
	  <42> when 'true' ->
	      'ok'
	  ( <_@c1> when 'true' ->
		( primop 'match_fail'
		      ({'function_clause',_@c1})
		  -| [{'function_name',{'e',1}}] )
	    -| ['compiler_generated'] )
	end
  • -| 将注释与 Core Erlang 构造关联。注释的含义未在 Core Erlang 语言规范中指定。

  • 与最后一个子句关联的 compiler_generated 注释是编译器添加的提示,如果发现该子句永远不匹配并被删除,则后续优化过程不应生成警告。

  • 开头的注释“Line 33”实际上是一个注释,美化打印机已将其转换为注释,以避免呈现美化打印的代码不可读。

结论 #

Core Erlang 比 Erlang 更简单,因此比抽象格式更适合代码分析工具(例如 Dialyzer)和优化器。

要了解有关 Core Erlang 的更多信息 #

所有详细信息都可以在 Core Erlang 1.0.3 语言规范中找到。