2021 年你需要知道的关于 Erlang 的一切
- admin
- 07 Jan 2024
今天,我们将看一个相当古老且有些古怪的东西。 你们大多数人可能没有注意到的语言。 虽然 Erlang 不像某些现代编程语言那样流行,但它安静地运行着 WhatsApp 和微信等每天为大量用户提供服务的应用程序。 在这篇文章中,我将告诉你关于这门语言的更多事情、它的历史,以及你是否应该考虑自己学习它。 ## 什么是 Erlang,它在哪里使用? Erl
Read More几天前,NextRoll 发布了 rebar3_hank,这是一个“强大而简单的工具,可以检测 Erlang 代码库周围的死代码”。在他们的原始帖子中,作者提到了 rebar3_hank 和 Erlang LS 提供的一些功能之间的重叠,例如检测未使用的包含文件。
对这个新工具很感兴趣,我决定深入研究它,检查 rebar3_hank 是否可以与 Erlang LS 中的诊断框架集成,以避免在 Erlang 社区内重复工作。
rebar3_hank 和 Erlang LS 都基于源代码创建诊断,但有一些区别。例如,rebar3_hank 作用于整个项目的代码库,而 Erlang LS 作用于单个 Erlang 模块及其严格的依赖关系。此外,rebar3_hank 旨在通过 rebar3 插件用作 CLI,而 Erlang LS 是通过 LSP 协议与您的 IDE 集成的服务器。
很明显,两个工具之间的集成需要一定程度的重构。不过,rebar3_hank 的想法非常有趣,其中大部分都可以在 Erlang LS 中轻松实现。因此,我决定移植其中一个,检测未使用的宏,并借此机会解释为 Erlang LS 贡献一个新的诊断后端的过程。
给定一个 Erlang 模块,如果定义了宏但未使用,我们希望收到警告通知。
我们怎样才能做到这一点?
我们需要做的第一件事是定义一个新的 Erlang 模块来实现 els_diagnostics 行为。 按照惯例,所有诊断模块都命名为 els_[BACKEND]_diagnostics.erl,在我们的例子中,[BACKEND] 将是未使用的宏。 因此,模块的最终名称将是 els_unused_macros_diagnostics.erl。 让我们打开一个新的文本文件并添加以下内容:
-module(els_unused_macros_diagnostics).
-behaviour(els_diagnostics).
els_diagnostics 行为需要实现三个回调函数:
-callback is_default() -> boolean().
-callback source() -> binary().
-callback run(uri()) -> [diagnostic()].
因此,让我们将以下导出添加到模块中。 我们将在一秒钟内实现所有功能。
-export([ is_default/0
, source/0
, run/1
]).
is_default/0 回调用于指定是否应默认启用当前后端。 在我们的例子中,我们希望默认启用新的后端,所以我们说:
is_default() -> true.
如果最终用户决定禁用此后端,她可以在她的 erlang_ls.config 中添加以下选项:
diagnostics:
disabled:
unused_macros
现在让我们实现我们的第二个回调函数,source/0。 此函数返回后端的人性化名称,由 IDE 呈现(请参见上面屏幕截图中的 UnusedMacros 文本):
source() -> <<"UnusedMacros">>.
我们需要实现的最后一个回调函数是有趣的事情发生的地方。
run/1 函数采用单个参数,即运行诊断的 Erlang 模块的 Uri。 默认情况下,诊断计算是 OnOpen(当模块首次在 IDE 中访问时)和 OnSave(当模块从 IDE 中保存时)。
该函数以 LSP 协议指定的格式返回将由 IDE 呈现的模块的诊断列表。 诊断可以有四种类型:
暂时,让我们返回一个空列表。
run(_Uri) -> [].
在我们可以在 Erlang LS 中使用我们的后端之前,我们还需要做一件事。 我们需要在可用的后端中注册它。 我们可以通过在 els_diagnostics 模块的可用诊断列表中添加一个条目来做到这一点:
available_diagnostics() ->
[ <<"compiler">>
, <<"crossref">>
, <<"dialyzer">>
, <<"elvis">>
, <<"unused_includes">>
, <<"unused_macros">> %% Here is our new shiny diagnostics backend!
].
此时,我们可以开始执行 run/1 函数的主体。 然后,为了尝试事情是否按预期工作,我们可以:
这种方法效果甚微,它会大大减慢我们的反馈循环,并且会使为 Erlang LS 做出贡献的整个体验变得痛苦。
幸运的是,有一个更好的方法:从测试用例开始。 毕竟,如果我们打算贡献我们的新后端,无论如何我们都需要添加一个测试用例。
但是,我们如何为这样的功能实现测试用例呢? 这听起来像很多工作,我们不知道从哪里开始。 这就是 Erlang LS 测试框架发挥作用的地方。
我们需要做的第一件事是向 Erlang LS 测试应用程序添加一个最小示例(由于历史原因,它被命名为 code_navigation,应该重命名)。 您可以在项目的 priv 目录中找到它。
我们的最小示例可能如下所示:
$ cat priv/code_navigation/src/diagnostics_unused_macros.erl
-module(diagnostics_unused_macros).
-export([main/0]).
-define(USED_MACRO, used_macro).
-define(UNUSED_MACRO, unused_macro).
main() ->
?USED_MACRO.
在上面的代码中,我们定义了两个宏,我们只使用了其中一个。 因此,我们希望在第 6 行发出UNUSED_MACRO警告。
我们还需要将我们的最小示例注册到 Erlang LS 测试框架中。 为此,我们在 els_test_utils 模块的源列表中添加一行:
sources() ->
[ ...
, diagnostics_unused_macros
, ...
]
现在,我们可以专注于实际的测试用例。 由于它是一个诊断测试,我们可以扩展已经存在的 els_diagnostics_SUITE 模块。 测试套件利用了 Erlang/OTP 中的 Common Test 框架,如果不熟悉请参考官方文档。
首先,我们导出新的测试用例,我们称之为unused_macros:
-export([ ...
, unused_macros/1
. ...
])
然后,我们可以实现新测试用例的主体:
unused_macros(Config) ->
Uri = ?config(diagnostics_unused_macros_uri, Config),
els_mock_diagnostics:subscribe(),
ok = els_client:did_save(Uri),
Diagnostics = els_mock_diagnostics:wait_until_complete(),
Expected = [ #{ message => <<"Unused macro: UNUSED_MACRO">>
, range =>
#{ 'end' => #{ character => 20
, line => 5
}
, start => #{ character => 8
, line => 5
}
}
, severity => ?DIAGNOSTIC_WARNING
, source => <<"UnusedMacros">>
}
],
?assertEqual(Expected, Diagnostics),
ok.
这里发生了很多事情,所以让我们一起来看代码,从头开始:
Uri = ?config(diagnostics_unused_macros_uri, Config),
在这里,我们从 Common Test Config 中获取包含我们最小示例的 Erlang 模块的 Uri。 这感觉有点神奇,因为我们从未在任何地方填充该变量。 这是怎么回事?
还记得我们在上面的 sources/1 函数中注册了我们的新测试模块吗? 这导致 Erlang LS 测试框架不仅索引该文件,还创建了几个方便的变量,可以在编写测试用例时使用。 [MODULE_NAME]_uri 是这些变量之一。 另一个是 [MODULE_NAME]_text,其中包含模块的实际源代码。 但是现在让我们继续我们的测试用例:
els_mock_diagnostics:subscribe(),
由于我们想测试一个新的诊断后端,我们订阅诊断流,以便我们可以拦截和验证它们。 同样,我们使用 Erlang LS 测试框架提供的实用功能。
ok = els_client:did_save(Uri),
Diagnostics = els_mock_diagnostics:wait_until_complete(),
然后,我们模拟 IDE 保存文件并等待诊断(异步计算)完成。
Expected = [ #{ message => <<"Unused macro: UNUSED_MACRO">>
, range =>
#{ 'end' => #{ character => 20
, line => 5
}
, start => #{ character => 8
, line => 5
}
}
, severity => ?DIAGNOSTIC_WARNING
, source => <<"UnusedMacros">>
}
],
?assertEqual(Expected, Diagnostics),
ok.
在这里,我们验证我们的后端是否生成了一条警告消息(注意 source 属性)。 警告消息预计在第 6 行,字符 8 和 20 之间(对应于宏名称的位置)。
?DIAGNOSTIC_WARNING 宏定义在 erlang_ls.hrl 头文件中,所以让我们将它包含在导出列表下方:
-include("erlang_ls.hrl").
现在让我们执行我们的测试用例并检查结果。 请注意我们需要如何为测试用例指定一个组,因为所有 Erlang LS 测试都可以针对两个 LSP 支持的传输(TCP 和 stdio)运行。
$ rebar3 ct --suite els_diagnostics_SUITE --case unused_macros --group tcp
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling erlang_ls
===> Running Common Test suites...
%%% els_diagnostics_SUITE:
[...]
expected: [#{message => <<"Unused macro: UNUSED_MACRO">>,
range =>
#{'end' => #{character => 20,line => 5},
start => #{character => 8,line => 5}},
severity => 2,source => <<"UnusedMacros">>}]
got: []
line: 582
不出所料,测试用例失败了,因为我们还没有实现 run/1 函数,但是我们返回了一个硬编码的空列表。 让我们现在解决这个问题。
我们终于可以跳到本教程的有趣部分,实现未使用宏的检测机制。 像往常一样,让我们先看看整个代码,然后解释它的行为。
run(Uri) ->
{ok, [Document]} = els_dt_document:lookup(Uri),
UnusedMacros = find_unused_macros(Document),
[make_diagnostic(Macro) || Macro <- UnusedMacros].
首先,我们通过 Uri 查询 Erlang LS 数据库。 然后,我们在返回的文档上调用 find_unused_macros/1 函数——我们仍然需要实现它。 对于每个已识别的宏,我们以 LSP 协议预期的格式生成诊断。 同样,我们仍然需要实现我们的 make_diagnostic/1 函数。
现在让我们关注 find_unused_macros/1 函数。 该函数的目标是识别给定文档中未使用的宏:
find_unused_macros(Document) ->
Definitions = els_dt_document:pois(Document, [define]),
Usages = els_dt_document:pois(Document, [macro]),
UsagesIds = [Id || #{id := Id} <- Usages],
[POI || #{id := Id} = POI <- Definitions, not lists:member(Id, UsagesIds)].
同样,这里发生了很多事情,所以让我们逐行浏览代码。
首先,我们识别所有的宏定义,由define 键识别:
Definitions = els_dt_document:pois(Document, [define]),
然后,我们识别所有的宏用法,通过macro key识别:
Usages = els_dt_document:pois(Document, [macro]),
您可以参考 Erlang LS 中的 els_parser 模块,了解有关 POI(兴趣点)和可用键的详细信息。
对于每个宏用法,我们提取各自的 id 并返回没有相应用法的宏定义列表:
UsagesIds = [Id || #{id := Id} <- Usages],
[POI || #{id := Id} = POI <- Definitions, not lists:member(Id, UsagesIds)].
最后缺少的位是 make_diagnostic/1 函数,它将每个 POI 转换为诊断:
make_diagnostic(#{id := Id, range := POIRange} = _POI) ->
Range = els_protocol:range(POIRange),
MacroName = atom_to_binary(Id, utf8),
Message = <<"Unused macro: ", MacroName/binary>>,
Severity = ?DIAGNOSTIC_WARNING,
Source = source(),
els_diagnostics:make_diagnostic(Range, Message, Severity, Source).
该函数本质上是实用函数 els_diagnostics:make_diagnostic/4 的包装。 让我们详细分析一下。
Range = els_protocol:range(POIRange),
这里我们使用 Erlang LS 提供的辅助函数将 POI 的范围(未使用的宏定义)转换为 LSP 协议所需的格式。
MacroName = atom_to_binary(Id, utf8),
Message = <<"Unused macro: ", MacroName/binary>>,
然后,我们使用有问题的宏的 id(名称)构建诊断消息。
Severity = ?DIAGNOSTIC_WARNING,
我们将指定为消息的严重程度为警告。
Source = source(),
我们调用 source/0 函数来指定诊断的来源(用于 IDE 中的渲染目的)。
els_diagnostics:make_diagnostic(Range, Message, Severity, Source).
最后,我们使用构造的参数调用 els_diagnostics:make_diagnostic/4 函数来生成诊断。
应该是这样的。 让我们再次尝试执行测试用例:
$ rebar3 ct --suite els_diagnostics_SUITE --case unused_macros --group tcp
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling erlang_ls
===> Running Common Test suites...
%%% els_diagnostics_SUITE:
All 1 tests passed.
成功! 现在测试通过了,我们能够识别 Erlang LS 中未使用的宏!
下面是后端的完整实现,供参考:
-module(els_unused_macros_diagnostics).
-behaviour(els_diagnostics).
-export([ is_default/0
, source/0
, run/1
]).
-include("erlang_ls.hrl").
is_default() -> true.
source() -> <<"UnusedMacros">>.
run(Uri) ->
{ok, [Document]} = els_dt_document:lookup(Uri),
UnusedMacros = find_unused_macros(Document),
[make_diagnostic(Macro) || Macro <- UnusedMacros].
find_unused_macros(Document) ->
Definitions = els_dt_document:pois(Document, [define]),
Usages = els_dt_document:pois(Document, [macro]),
UsagesIds = [Id || #{id := Id} <- Usages],
[POI || #{id := Id} = POI <- Definitions, not lists:member(Id, UsagesIds)].
make_diagnostic(#{id := Id, range := POIRange} = _POI) ->
Range = els_protocol:range(POIRange),
MacroName = atom_to_binary(Id, utf8),
Message = <<"Unused macro: ", MacroName/binary>>,
Severity = ?DIAGNOSTIC_WARNING,
Source = source(),
els_diagnostics:make_diagnostic(Range, Message, Severity, Source).
当然,上面的实现非常简单,可以在很多方面进行改进,但是此时您应该了解为 Erlang LS 实现新的诊断后端意味着什么。
上面的后端已经集成在 Erlang LS 中,但作为一个可选的后端。 您可以在以下位置查看全部贡献:
为开源做贡献可能是一种令人生畏的经历,尤其是当您没有无限的可用时间时。 我希望这个小教程可以帮助你朝这个方向发展,我期待着你能为 Erlang LS 做出贡献。
玩得开心!
今天,我们将看一个相当古老且有些古怪的东西。 你们大多数人可能没有注意到的语言。 虽然 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