2021 年你需要知道的关于 Erlang 的一切
- admin
- 07 Jan 2024
今天,我们将看一个相当古老且有些古怪的东西。 你们大多数人可能没有注意到的语言。 虽然 Erlang 不像某些现代编程语言那样流行,但它安静地运行着 WhatsApp 和微信等每天为大量用户提供服务的应用程序。 在这篇文章中,我将告诉你关于这门语言的更多事情、它的历史,以及你是否应该考虑自己学习它。 ## 什么是 Erlang,它在哪里使用? Erl
Read MoreErlang 是一个先进的分布式软件平台,非常适合网络和多核计算。我相信它几乎已经为 Web 编程的“黄金时间”做好了准备,但至少有一个大问题阻碍了 Erlang 平台:Erlang语言不适合 Web 程序员面临的许多问题。例如:
我将讨论一种规避这些问题的方法,即实现另一种语言以在 Erlang 平台上运行。如果您有野心,您可能会实现一种功能齐全的编程语言,但在这里我将讨论为简单的模板语言编写编译器。
Erlang 平台是一个虚拟机。Erlang 代码被编译成称为 BEAM 的二进制代码(字节代码),但它在此过程中采用了一些中间形式。作为编译器编写者,您首先需要选择您的目标平台,它可以是以下之一:
要针对 #2、3 或 4,您需要了解有关 Erlang 内部结构的更多信息。我对 Erlang 的内部了解不多,所以我将针对 #1,即 Erlang 语言。也就是说,我的编译器只是将模板文件转换为 Erlang 代码,然后调用标准的 Erlang 编译器。所以实际上,我们在作弊,但最终我们仍在生产编译模块,没有人知道我们作弊了。
首先你需要选择一种语言来编译。如果有语言规范,它会有所帮助。我正在实现 Django 模板语言。没有真正的规范,但 Python 中有一个官方实现,所以每当我不确定某些事情时,我可以查阅它。
本教程的其余部分以我和 Roberto Saccon 的ErlyDTL项目为例。
实现语言的第一步是编写一个扫描器。扫描器读入一个文件并决定什么是令牌。标记可以是字符串、括号、标识符、语言关键字或类似的东西。扫描器只返回一个标记的平面列表,其中包含有关每个标记所在行的一些信息。使用 Erlang,您可以使用名为 Leex 的程序来帮助您编写扫描器,或者您可以使用普通的 Erlang 编写扫描器。当我写扫描器的时候我不知道Leex,所以我们只是用老式的方式来做。
这是 ErlyDTL 的扫描器模块 ( erlydtl_scanner.erl ) 中的重要内容。首先是无聊的东西:
-module(erlydtl_scanner).
-export([scan/1]).
scan/1将接收代表我们正在编译的源代码的列表。
scan(Template) ->
scan(Template, [], {1, 1}, in_text).
如您所见,它只是调用scan/4,其参数是:
状态只是我们传递的一个原子,它告诉我们是否处于扫描过程中,例如我们是否在等待一个字符串关闭。这就像您的计算理论课程中的有限状态机中的状态。实际上,我们的扫描器将是一台有限状态机。每个函数子句都将定义一个转换规则。这是一个这样的规则:
scan("{{" ++ T, Scanned, {Row, Column}, in_text) ->
scan(T, [{open_var, {Row, Column}, "{{"} | Scanned], {Row, Column+2}, {in_code, "}}"});
这意味着“如果我们处于 in_text 状态并看到{{,那么:
很酷,我们如何在两行 Erlang 中完成所有四件事。因为它是 Erlang 是非常好的扫描语言。以下是处理 和 之间的代码注释的一些扫描{#规则#}:
scan("{#" ++ T, Scanned, {Row, Column}, in_text) ->
scan(T, Scanned, {Row, Column + 2}, {in_comment, "#}"});
scan([_ | T], Scanned, {Row, Column}, {in_comment, Closer}) ->
scan(T, Scanned, {Row, Column + 1}, {in_comment, Closer});
scan("#}" ++ T, Scanned, {Row, Column}, {in_comment, "#}"}) ->
scan(T, Scanned, {Row, Column + 2}, in_text);
这里还有一些仅用于处理模板文件的非代码部分的扫描规则。
scan("
" ++ T, Scanned, {Row, Column}, in_text) ->
scan(T, append_text_char(Scanned, {Row, Column}, $
), {Row + 1, 1}, in_text);
scan([H | T], Scanned, {Row, Column}, in_text) ->
scan(T, append_text_char(Scanned, {Row, Column}, H), {Row, Column + 1}, in_text);
他们只是增加行和列的位置。注意使用append_text_char. 当前字符应该附加到前一个标记上,因此我们定义了一个函数来为我们跳过:
append_text_char(Scanned, {Row, Column}, Char) ->
case length(Scanned) of
0 ->
[{text, {Row, Column}, [Char]}];
_ ->
[Token | Scanned1] = Scanned,
case element(1, Token) of
text ->
[{text, element(2, Token), [Char | element(3, Token)]} | Scanned1];
_ ->
[{text, element(2, Token), [Char]} | Scanned]
end
end.
以下规则处理在 Django 模板语言中具有特殊含义的字符:
scan("," ++ T, Scanned, {Row, Column}, {_, Closer}) ->
scan(T, [{comma, {Row, Column}, ","} | Scanned], {Row, Column + 1}, {in_code, Closer});
scan("|" ++ T, Scanned, {Row, Column}, {_, Closer}) ->
scan(T, [{pipe, {Row, Column}, "|"} | Scanned], {Row, Column + 1}, {in_code, Closer});
scan("=" ++ T, Scanned, {Row, Column}, {_, Closer}) ->
scan(T, [{equal, {Row, Column}, "="} | Scanned], {Row, Column + 1}, {in_code, Closer});
scan(":" ++ T, Scanned, {Row, Column}, {_, Closer}) ->
scan(T, [{colon, {Row, Column}, ":"} | Scanned], {Row, Column + 1}, {in_code, Closer});
scan("." ++ T, Scanned, {Row, Column}, {_, Closer}) ->
scan(T, [{dot, {Row, Column}, "."} | Scanned], {Row, Column + 1}, {in_code, Closer});
有点冗长,但无论如何。这应该为您提供规则的主要思想。最后,当我们到达输入的末尾时,我们需要反转我们的标记列表并标记我们遇到的任何语言关键字。
scan([], Scanned, _, in_text) ->
{ok, lists:reverse(lists:map(
fun
({identifier, Pos, String}) ->
RevString = lists:reverse(String),
Keywords = [ "if", "else", "endif", "not" ], %many others too
Type = case lists:member(RevString, Keywords) of
true ->
list_to_atom(RevString ++ "_keyword");
_ ->
identifier
end,
{Type, Pos, RevString};
({Type, Pos, String}) ->
{Type, Pos, lists:reverse(String)}
end, Scanned))};
我们完成了。扫描器返回{ok, Tokens}。是时候将这些令牌发送到解析器了。
解析器获取令牌列表并将它们排列成称为解析树的树结构。你可能在你的第一个 CS 课上做了类似的事情,你取“1+1”并制作一棵树,其中“+”作为父级,“1”和“1”作为子级。这里的想法相同。
我们以名为Yecc的解析器生成器的形式提供了一些帮助。Yecc 与 yacc 类似,但适用于 Erlang。我们将告诉它相邻的令牌是如何相互关联的,剩下的交给 Yecc。
我们的 Yecc 文件如下所示。它包含三个部分:非终结符列表、终结符列表和规则列表。非终端是您可以扩展为终端和非终端列表的东西。终端无法扩展。使用规则,我们将所有非终端扩展为终端。
这是我们的 Django 模板语言 Yecc 文件 ( erlydtl_parser.yrl ) 的子集,足以处理“if”语句。
Nonterminals
Elements
Literal
Value
Variable
IfBlock
IfBraced
IfExpression
ElseBraced
EndIfBraced.
Terminals
close_tag
endif_keyword
if_keyword
else_keyword
dot
open_tag
not_keyword
text.
Rootsymbol
Elements.
这是规则。格式为 [要扩展的东西] -> [扩展的组件] : [将组件重新组合成列表或树的 Erlang 表达式]。在最高级别,我们只是将事物放入一个列表中:
Elements -> '$empty' : [].
Elements -> Elements text : '$1' ++ ['$2'].
Elements -> Elements IfBlock : '$1' ++ ['$2'].
嗯,这很有趣。一个IfBlock呢?
IfBlock -> IfBraced Elements ElseBraced Elements EndIfBraced : {ifelse, '$1', '$2', '$4'}.
IfBlock -> IfBraced Elements EndIfBraced : {'if', '$1', '$2'}.
使用元组,我们构建了一棵树。所有这些非终端扩展到什么?
IfBraced -> open_tag if_keyword IfExpression close_tag : '$3'.
IfExpression -> not_keyword IfExpression : {'not', '$2'}.
IfExpression -> Value : '$1'.
最后得到一些终端。for 的规则Value有点复杂,这里就不赘述了。
正如您在下面看到的,冒号后面的部分是严格可选的。在这种情况下,解析树中不会添加任何内容。
ElseBraced -> open_tag else_keyword close_tag.
EndIfBraced -> open_tag endif_keyword close_tag.
我们确实需要将 Yecc 文件编译成 Erlang 代码。Yecc 是内置在 erlc 中的,所以你可以运行erlc -o src/ src/myfile.yrl它,它应该会输出一个 erl 文件。
ErlyDTL 的 Yecc 文件总共有大约 200 行,与扫描器的长度大致相同。400 行代码,我们完成了 2/3 的编译器!好样的。
现在我们有一个解析树。我们要做的就是把它变成 Erlang 代码。实际上,我们将借助 Erlang 模块将其转换为抽象语法树erl_syntax(AST) 。然后我们可以调用一个特殊的函数将 AST 编译成字节码。构建一个 AST 而不是 Erlang ASCII 代码使我们的代码感觉更干净,更容易调试,尽管有时它看起来很冗长。
erl_syntax有一个对应于 Erlang 中每个句法结构的方法。如果你想构建一个原子,你调用erl_syntax:atom/1. 如果要构建函数调用,请调用erl_syntax:application/3. 等等。查看erl_syntax手册页了解详细信息。
编译器代码可以在erlydtl_beam_compiler.erl中找到。
编译器最难看的部分是为已编译模块的骨架制作 AST。我们需要一个 AST 用于-module定义、-export语句和每个编译模板将具有的“渲染”函数。在 ErlyDTL 中,这是在一个名为 erlydtl_beam_compiler:forms/5 ( source ) 的函数中完成的,这可能会让您头疼。您可以根据自己的时间阅读。它的关键部分是对 的调用erl_syntax:revert,它将 AST 转换为称为“表单”的表示,然后您将其直接发送到compile:forms/2.
ErlyDTL 编译器最漂亮的部分也是最精妙的部分。它是一个名为body_ast/3( source ) 的函数,它查看解析树上的每个节点并将其转换为适当的 Erlang AST 节点,然后递归地对每个节点的子节点执行相同的操作。当我们向下解析树时,我们需要传递一些上下文,特别是当前范围内的所有变量。我们可能会在向下解析树时拾取一些范围,并在我们返回时将它们丢弃。
让我们看一下body_ast的片段。它所做的只是获取解析树的顶层并决定如何处理每个父节点。这是处理“if”语句的部分。
body_ast(DjangoParseTree, Context) ->
lists:map(
fun
%...
({'if', Variable, Contents}) ->
ifelse_ast(Variable, body_ast(Contents, Context), empty_ast(), Context);
({'if', {'not', Variable}, Contents}) ->
ifelse_ast(Variable, empty_ast(), body_ast(Contents, Context), Context);
({'ifelse', Variable, IfContents, ElseContents}) ->
ifelse_ast(Variable, body_ast(IfContents, Context),
body_ast(ElseContents, Context), Context);
%...
end, DjangoParseTree).
那个代码是不是就像五月的清风一样?好吧,也许不是,但这样的代码是生活中简单的乐趣之一。这是递归下降编译器的基本结构。该函数处理每个节点并将子节点传递回body_ast. 对于此处的“if”语句,它调用一个函数,该函数ifelse_ast构建一个 AST 以测试是否Variable评估为真并运行适当的代码块。如果没有“else”块,那么它会传入一个空的 AST,这样如果变量为假,就不会运行任何代码。这是ifelse_ast:
ifelse_ast(Variable, IfContentsAst, ElseContentsAst, Context) ->
% first resolve the variable to the right scope
{Ast, VarName} = resolve_ifvariable_ast(Variable, Context),
% now write an if statement
erl_syntax:case_expr(erl_syntax:application(
erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(is_false), [Ast]),
[erl_syntax:clause([erl_syntax:atom(true)], none,
[ElseContentsAst]),
erl_syntax:clause([erl_syntax:underscore()], none,
[IfContentsAst])
]).
现在好了,你应该对 ErlyDTL 编译器有了基本的了解。编译器部分重约 750 行。我们用一些编译模块所依赖的运行时代码作弊,所以我们的全功能模板编译器大约有 2000 行代码。不过是你可以在一个下午就能完成编码的东西,也不是一项艰巨的任务。
Erlang 几乎可以肯定是未来的服务器平台,但它需要一些创造性的想法才能让 Web 编程更愉快。实现模板语言是一个开始,但为什么不为模型和控制器也提供新的语言呢?他们说 Rails 之所以成为可能,是因为 Ruby 强大的元编程特性。如果有一个与 Erlang 等效的 Rails,这将是可能的,因为为 Erlang 平台编写编译器很容易。
今天,我们将看一个相当古老且有些古怪的东西。 你们大多数人可能没有注意到的语言。 虽然 Erlang 不像某些现代编程语言那样流行,但它安静地运行着 WhatsApp 和微信等每天为大量用户提供服务的应用程序。 在这篇文章中,我将告诉你关于这门语言的更多事情、它的历史,以及你是否应该考虑自己学习它。 ## 什么是 Erlang,它在哪里使用? Erl
Read More这篇文章探讨了 Erlang/OTP 25 中基于类型的新优化,其中编译器将类型信息嵌入到 BEAM 文件中,以帮助JIT(即时编译器)生成更好的代码。 ## 两全其美 OTP 22 中引入的基于SSA的编译器处理步骤进行了复杂的类型分析,允许进行更多优化和更好的生成代码。然而,Erlang 编译器可以做什么样的优化是有限制的,因为 BEAM 文件必须
Read More自从Erlang 存在,就一直有让它更快的需求和野心。这篇博文是一堂历史课,概述了主要的 Erlang 实现以及如何尝试提高 Erlang 的性能。 ## Prolog 解释器 Erlang 的第一个版本是在 1986 年在 Prolog 中实现的。那个版本的 Erlang 对于创建真正的应用程序来说太慢了,但它对于找出Erlang语言的哪些功能有用,哪
Read More