2021 年你需要知道的关于 Erlang 的一切
- admin
- 07 Jan 2024
今天,我们将看一个相当古老且有些古怪的东西。 你们大多数人可能没有注意到的语言。 虽然 Erlang 不像某些现代编程语言那样流行,但它安静地运行着 WhatsApp 和微信等每天为大量用户提供服务的应用程序。 在这篇文章中,我将告诉你关于这门语言的更多事情、它的历史,以及你是否应该考虑自己学习它。 ## 什么是 Erlang,它在哪里使用? Erl
Read More选择 Run and Debug 面板,从下拉列表中选择 Existing Erlang Node 并按下播放按钮:
打开一个新终端并使用 curl 命令触发我们的新断点。
curl -i http://localhost:8080
HTTP/1.1 200 OK
content-length: 12
content-type: text/plain
date: Fri, 09 Jul 2021 13:35:01 GMT
server: Cowboy
Hello world!
执行将在断点处暂停。 然后,您可以使用标准的 VS Code 控件来控制执行:
在左侧,可以探索调用堆栈和变量绑定。 例如,我们可以逐步扩展 Cowboy 输入请求的绑定并验证 User Agent 标头的值:
底部的调试控制台可用作 REPL,并提供当前的变量绑定:
左侧的Watch列表可用于跟踪特定变量的值(例如,Opts 变量):
以及用于操作这些值的调试控制台:
VS Code 提供了广泛的调试功能。 更多信息请参考官方 VS Code 文档。
打开 src/toppage_h.erl 并运行:
M-x dap-debug
系统将提示您输入配置模板。 选择现有的 Erlang 节点。
打开一个新终端并使用 curl 命令触发我们的新断点。
curl -i http://localhost:8080
HTTP/1.1 200 OK
content-length: 12
content-type: text/plain
date: Fri, 09 Jul 2021 13:35:01 GMT
server: Cowboy
Hello world!
执行将在断点处暂停。 然后,您可以使用标准 Emacs 控件来控制执行:
在右侧,可以探索调用堆栈和变量绑定。 例如,我们可以逐步扩展 Cowboy 输入请求的绑定并验证 User Agent 标头的值:
您还可以使用当前可用的变量绑定打开 REPL:
M-x dap-eval
dap-mode 包提供了广泛的调试功能。 更多信息请参考官方文档。
DAP 协议描述了可以在不同情况下使用的多种断点类型:
仅当给定条件评估为真时才会触发条件断点。 例如,我们可能希望仅当客户端传递的 Host 标头的值包含字符串 pigeon 时才中断执行:
要设置条件断点,请右键单击行号旁边并选择添加条件断点…选项。 添加以下表达式:
maps:get(<<"host">>, maps:get(headers, Req0)) =:= <<"pigeon">>
要添加条件断点,请移动到现有断点,然后运行:
M-x dap-breakpoint-condition
并添加以下表达式:
maps:get(<<"host">>, maps:get(headers, Req0)) =:= <<"pigeon">>
设置了上述条件断点后,以下请求不会导致执行中断:
curl -i http://localhost:8080
但以下将会触发断点:
curl -H "Host: pigeon" -i http://localhost:8080
日志断点是一种特殊类型的断点,它不会导致执行中断,但它们会导致在调试控制台中打印出一条日志消息。
要记录每个请求的 Host 标头,请右键单击行号旁边并选择 Add logpoint… 选项。 添加以下日志消息:
maps:get(<<"host">>, maps:get(headers, Req0))
让我们用不同的(或默认的)主机头触发一些请求:
curl -i http://localhost:8080
curl -H "Host: pigeon" -i http://localhost:8080
然后我们可以在调试控制台中跟踪日志断点:
要记录每个请求的 Host 标头,请移动到现有断点,然后运行:
M-x dap-breakpoint-log-message
添加以下日志消息:
maps:get(<<"host">>, maps:get(headers, Req0))
让我们用不同的(或默认的)主机头触发一些请求:
curl -i http://localhost:8080
curl -H "Host: pigeon" -i http://localhost:8080
要跟踪日志断点,请运行:
M-x dap-go-to-output-buffer
Hitpoints是一种特殊的断点,每 N 次触发一次。
选择一个现有断点并从下拉列表中选择 Hit Count 选项。 指定一个数字 N。相应的断点将每第 N 次触发一次。
导航到现有断点。 运行:
M-x dap-breakpoint-hit-condition
指定一个数字 N。相应的断点将每第 N 次触发一次。
如果某些事情没有按预期工作,请查看 Erlang LS DAP 日志。 他们很可能会指出问题的根本原因。 日志可在以下位置获得:
[USER_LOG_DIR]/[PROJECT_NAME]/dap_server.log
其中 [USER_LOG_DIR] 是以下输出:
filename:basedir(user_log, "els_dap").
例如,在 Mac OS 上,hello_world 项目的 DAP 日志将位于:
/Users/[USERNAME]/Library/Logs/els_dap/hello_world/dap_server.log
如果 DAP 日志没有帮助,请随时在 GitHub 或 Slack 上联系。
使用 Erlang LS 进行愉快的调试!
在我们之前的教程中,我们学习了如何为 Erlang 语言服务器实现诊断后端。 这次我们将深入了解 Code Lenses 的世界。
给定一个包含许多函数定义的 Erlang 模块,我们希望在其各自定义之上显示对每个函数的引用数。 这是code lenses在 VS Code 中的外观。
在本教程结束时,您将:
事不宜迟,让我们开始吧。
Wade Anderson 将 Code Lens 定义为:
散布在您的源代码中的可操作的上下文信息
这是一种非常奇特的说法,即code lenses是出现在 IDE 中的位于您的代码旁边的任意一段文本。 文本通常会提供有关部分代码的见解,就像我们刚刚在上面看到的示例一样。
code lenses也可以是可操作的。 用户可以通过单击lenses或使用键盘快捷键来激活lenses以执行操作。 触发的动作可以是任何东西。 这是一个 Emacs code lenses,它允许用户执行给定的 Common Test 测试用例:
code lenses是上下文相关的,这意味着它们知道周围的上下文。 在上面的例子中,运行测试lenses知道点击时应该执行哪个特定的测试用例。
现在我们了解了什么是code lenses,让我们在 Erlang LS 中实现一个。
Erlang LS 提供了一个框架,使code lenses的开发尽可能简单。 要创建我们的新的code lenses,我们需要做的第一件事是为其命名并创建一个新的 Erlang 模块来实现 els_code_lens 行为。 让我们将新代码称为lens function_references。
-module(els_code_lens_function_references).
-behaviour(els_code_lens).
els_code_lens 行为需要实现三个回调函数:
-callback is_default() -> boolean().
-callback pois(els_dt_document:item()) -> [poi()].
-callback command(els_dt_document:item(), poi(), state()) -> els_command:command().
稍后我们将看到每个回调函数应该做什么。 现在,让我们将以下函数导出添加到我们的 els_code_lens_function_references 模块中:
-export([ is_default/0
, pois/1
, command/3
]).
现在我们可以专注于每个单独的回调函数。
is_default/0 回调用于指定是否应默认启用当前后端。 在我们的例子中,我们希望默认启用新的后端,所以我们说:
is_default() -> true.
如果最终用户决定禁用此后端,她可以在她的 erlang_ls.config 中添加以下选项:
lenses:
disabled:
function_references
在 Erlang LS 行话中,POI 代表兴趣点。 该术语指的是作为代码库一部分的有兴趣的点位。 兴趣点由 Erlang LS 索引并存储在内存数据库中。 POI 可以指代函数定义、宏定义、记录用法,应有尽有。 Erlang LS 提供了一组实用程序,可以轻松搜索和操作兴趣点。
pois/1 函数接受一个参数,即当前文档。 它的返回值是应该为其激活lenses的 POI 列表。 在我们的例子中,我们希望我们的lenses在每个函数定义旁边都是可见的。 因此,我们写:
pois(Document) ->
els_dt_document:pois(Document, [function]).
我们需要实现的最后一个强制回调是 command/3 之一。 回调接受三个参数:当前文档、特定 POI 和状态。 为了本教程的目的,我们将忽略状态并仅关注前两个参数。
该函数需要返回一个命令。 命令是一个 LSP 数据结构,它包含:
Erlang LS 提供了一个辅助函数来创建这样的数据结构: els_command:make_command/3 函数。 然后,我们的 command/3 函数将如下所示:
command(Document, POI, _State) ->
Title = title(Document, POI),
CommandId = command_id(),
CommandArgs = command_args(),
els_command:make_command(Title, CommandId, CommandArgs).
我们现在将详细描述每个参数并学习如何计算它们,从标题开始。
标题是我们想要在文本编辑器中显示的文本,位于我们的兴趣点(又名 POI)旁边。 在我们的例子中,我们想要显示以下文本:
Used [N] times
为了能够计算数字 N,我们需要知道有多少对当前函数的引用分布在我们的代码库中。 因此,我们可以通过 els_dt_references:find_by_id/2 辅助函数查询 Erlang LS 数据库:
title(Document, POI) ->
%% Extract the module name from the current document
#{uri := Uri} = Document,
M = els_uri:module(Uri),
%% Extract the function name and arity from the current POI
#{id := {F, A}} = POI,
%% Query the Erlang LS DB for references to the current function
{ok, References} = els_dt_references:find_by_id(function, {M, F, A}),
%% Calculate the number of references
N = length(References),
%% Format the title for the code lens
unicode:characters_to_binary(io_lib:format("Used ~p times", [N])).
els_dt_references:find_by_id/2 函数有两个参数:我们正在寻找的引用类型(在我们的例子中是函数)和当前兴趣点的完全限定 ID。 对于函数定义,完全限定标识符是 {M, F, A} 元组,代表模块、函数名称和函数的 Arity。 如上所示,我们可以从 Document 中提取模块 M,从当前 POI 中提取 F 和 A。
CommandId 是当用户点击我们的code lenses时我们想要运行的命令的任意标识符。 在我们的例子中,这个动作将是一个空操作,但我们仍然需要为我们的命令选择一个名称。 我们称之为函数引用:
command_id() -> <<"function-references">>.
由于我们的命令是无操作的(如果用户点击lenses,我们不希望发生任何事情),我们的命令不需要任何参数:
command_args() -> [].
我们基本上完成了。 为了完整起见,这是我们完整的 els_code_lens_function_references 模块:
-module(els_code_lens_function_references).
-behaviour(els_code_lens).
-export([ is_default/0
, pois/1
, command/3
]).
is_default() ->
true.
pois(Document) ->
els_dt_document:pois(Document, [function]).
command(Document, POI, _State) ->
Title = title(Document, POI),
CommandId = command_id(),
CommandArgs = command_args(),
els_command:make_command(Title, CommandId, CommandArgs).
title(Document, POI) ->
#{uri := Uri} = Document,
M = els_uri:module(Uri),
#{id := {F, A}} = POI,
{ok, References} = els_dt_references:find_by_id(function, {M, F, A}),
N = length(References),
unicode:characters_to_binary(io_lib:format("Used ~p times", [N])).
command_id() ->
<<"function-references">>.
command_args() ->
[].
在使用新的code lenses之前,我们还需要做一件事:我们需要告诉 Erlang LS 它的存在。 这可以通过将我们的新的code lenses添加到 els_code_lens 模块中的 available_lenses 列表中来实现:
available_lenses() ->
[ ...
, <<"function-references">>
].
就这样。
此时我们的code lenses应该可以正常工作,但在为它编写测试之前我们无法确定! Erlang LS 提供了一个可用于此目的的测试框架。 在本节中,我们将假设您已经对 Erlang LS 测试框架有所了解。 如果您想更温和地介绍 Erlang LS 中的测试,请参阅之前的诊断教程。
让我们在 code_navigation 测试应用程序中创建一个名为 code_lens_function_references 的测试模块:
$ cat apps/els_lsp/priv/code_navigation/src/code_lens_function_references.erl
-module(code_lens_function_references).
-export([ a/0 ]).
-spec a() -> ok.
a() ->
b(),
c().
-spec b() -> ok.
b() ->
c().
-spec c() -> ok.
c() ->
ok.
只需打开 els_test_utils 模块并将新模块添加到源列表中。 这将确保新模块被正确索引并且一些辅助函数可用。
sources() ->
[ ...
, code_lens_function_references
, ...
].
然后让我们打开 els_code_lens_SUITE 模块并添加一个测试用例,我们在其中检查新的code lenses在新模块中是否按预期工作。
function_references(Config) ->
Uri = ?config(code_lens_function_references_uri, Config),
#{result := Result} = els_client:document_codelens(Uri),
Expected = [ lens(5, 0) # First lens on line 5, 0 references
, lens(10, 1) # Second lens on line 10, 1 reference
, lens(14, 2) # Third lens on line 14, 2 references
],
?assertEqual(Expected, Result),
ok.
在上面的测试用例中,我们通过利用 Erlang LS 测试框架来获取新添加的测试模块的 Uri。 然后,我们使用 els_client 为给定的 Uri 调用 document_codelens 方法,最终确保我们收到预期的code lenses列表。 lens/2 是一个辅助函数,它构造了 LSP 协议所期望的数据结构如下:
lens(Line, Usages) ->
Title = unicode:characters_to_binary(
io_lib:format("Used ~p times", [Usages])),
#{ command =>
#{ arguments => []
, command => els_command:with_prefix(<<"function-references">>)
, title => Title
}
, data => []
, range =>
#{ 'end' => #{character => 1, line => Line}
, start => #{character => 0, line => Line}
}
}.
让我们运行测试并确保它通过。
$ rebar3 ct --suite apps/els_lsp/test/els_code_lens_SUITE --case function_references --group tcp
[...]
===> Running Common Test suites...
%%% els_code_lens_SUITE: .
All 1 tests passed.
看起来我们在这里完成了。
init/1 回调允许我们对每个文件执行一次计算,并将计算值以 State 的形式传递给后续回调函数(还记得我们在 command/3 回调中忽略的 State 参数吗?)。 例如,在建议规范code lenses中使用它来为每个 Erlang 模块运行一次 TypEr,并且仍然能够为每个函数显示一个lens。
precondition/1 回调允许我们只为特定类型的文档启用给定的lens。 例如,以下实现仅对 Common Test 套件启用 ct_run_test lens,由 ct.hrl 文件的 include_lib 指令标识:
precondition(Document) ->
Includes = els_dt_document:pois(Document, [include_lib]),
case [POI || #{id := "common_test/include/ct.hrl"} = POI <- Includes] of
[] ->
false;
_ ->
true
end.
此时,您应该可以尝试新的code lenses。 上面的code lenses已经在 Erlang LS 中可用。 您可以在以下位置查看全部贡献。我希望本教程能帮助您更好地理解code lenses以及如何在 Erlang LS 中实现code lenses。 期待您将实施的精彩lens!
今天,我们将看一个相当古老且有些古怪的东西。 你们大多数人可能没有注意到的语言。 虽然 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