Elixir v1.11发布——在编译时捕捉更多的错误

117 阅读9分钟

在过去的几个版本中,Elixir团队一直专注于编译器,无论是在编译时捕捉更多的错误,还是使其更快。Elixir v1.11在这两方面都取得了卓越的进展。这个版本还包括许多其他好东西,如更紧密的Erlang集成,支持更多的guard表达式,内置的日期时间格式化,以及其他日历增强。

在此期间,我们也开始在网站上发布一系列生产案例,介绍Elixir在BrexFarmbotHeroku的使用情况,还有更多案例即将发布。

现在,让我们关注一下Elixir v1.11的新内容。

更紧密的Erlang集成

在v1.10版本的基础上,我们进一步整合了Erlang的新日志器,增加了四个新的日志级别。notice,critical,alert, 和emergency, 匹配Syslog标准中的所有日志级别。Logger 模块现在支持结构化日志,通过传递地图和关键字列表给它的各种函数。也可以通过函数指定每个模块的日志级别。 Logger.put_module_level/2函数指定每个模块的日志级别。每个应用程序的日志级别将在未来的版本中添加。

IEx也得到了改进,可以直接从Elixir终端显示Erlang模块的文档。例如,这里有一个我访问Erlang的gen_server模块文档的片段。

见asciinema中的例子

这适用于Erlang/OTP 23+,并且要求Erlang模块已经被编译了文档块。非常感谢Erlang/OTP团队和Erlang Ecosystem Foundation的文档工作组使这成为可能。

编译器检查:应用程序的边界

Elixir v1.11建立在最近增加的编译追踪器之上,以追踪应用程序的边界。从这个版本开始,如果你从一个现有的模块中调用一个函数,但这个模块不属于你列出的任何依赖关系,Elixir会发出警告。

这两个条件似乎是相互矛盾的。毕竟,如果一个模块是可用的,它一定来自于一个依赖关系。这在两种情况下是不正确的。

  • 来自Elixir和Erlang/OTP的模块总是可用的--即使它们的应用没有被列为依赖关系

  • 在一个伞状项目中,由于所有的子程序都是在同一个虚拟机中编译的,你可能会有一个来自兄弟项目的模块可用,即使你并不依赖该兄弟项目。

这个新的编译器检查确保所有你调用的模块都被列为你的依赖关系的一部分,否则就会发出如下警告。

:ssl.connect/2 defined in application :ssl is used by the current
application but the current application does not directly depend
on :ssl. To fix this, you must do one of:

  1. If :ssl is part of Erlang/Elixir, you must include it under
     :extra_applications inside "def application" in your mix.exs

  2. If :ssl is a dependency, make sure it is listed under "def deps"
     in your mix.exs

  3. In case you don't want to add a requirement to :ssl, you may
     optionally skip this warning by adding [xref: [exclude: :ssl]
     to your "def project" in mix.exs

这在伞状项目中带来了额外的好处,因为它要求应用程序依赖它们所依赖的兄弟姐妹,如果有任何循环的依赖,这将会失败。

编译器检查:数据构造函数

在Elixir v1.11中,编译器还跟踪结构体,并在一个函数体中映射字段。例如,想象你想写这样的代码。

def drive?(%User{age: age}), do: age >= 18

如果:age 字段上有错别字,或者:age 字段还没有定义,编译器就会相应地失败。然而,如果你写这样的代码。

def drive?(%User{} = user), do: user.age >= 18

编译器就不会捕捉到这个缺失的字段,只有在运行时才会出现错误。在v1.11版本中,Elixir将跟踪同一函数中所有maps和struct字段的使用情况,对类似上述情况发出警告。

warning: undefined field `age` in expression:

    # example.exs:7
    user.age

expected one of the following fields: name, address

where "user" was given the type %User{} in:

    # example.exs:7
    %User{} = user

Conflict found at
  example.exs:7: Check.drive?/1

编译器也会检查二进制构造函数。考虑到你必须在电线上发送一个基于长度的编码的字符串,其中字符串以其长度为前缀,最大为4MB。你最初的尝试可能是这样的。

def run_length(string) when is_binary(string) do
  <<byte_size(string)::32, string>>
end

然而,上面的代码有一个错误。在<<>> 之间给出的每个段必须是一个整数,除非另有规定。在Elixir v1.11中,编译器会让你知道这一点。

warning: incompatible types:

    binary() !~ integer()

in expression:

    <<byte_size(string)::integer()-size(32), string>>

where "string" was given the type integer() in:

    # foo.exs:4
    <<byte_size(string)::integer()-size(32), string>>

where "string" was given the type binary() in:

    # foo.exs:3
    is_binary(string)

HINT: all expressions given to binaries are assumed to be of type integer()
unless said otherwise. For example, <<expr>> assumes "expr" is an integer.
Pass a modifier, such as <<expr::float>> or <<expr::binary>>, to change the
default behaviour.

Conflict found at
  foo.exs:4: Check.run_length/1

这可以通过在第二个组件中添加::binary 来解决。

def run_length(string) when is_binary(string) do
  <<byte_size(string)::32, string::binary>>
end

虽然其中一些警告可以由编译器自动修复,但未来的版本也会跨函数和潜在的跨模块执行这些检查,而自动修复是不希望的(也不可能)。

编译时间的改进

Elixir v1.11对编译器跟踪文件依赖关系的方式进行了许多改进,例如,触摸一个文件会导致更少的文件被重新编译。在以前的版本中,Elixir跟踪了三种类型的依赖关系。

  • 编译时依赖 - 如果A在编译时依赖B,例如使用宏,只要B改变,A就会被重新编译
  • 结构依赖--如果A依赖B的结构,只要B的结构定义发生变化,A就会被重新编译。
  • 运行时依赖--如果A在运行时依赖B,A永远不会被重新编译。

然而,由于依赖关系是传递性的,如果A在编译时依赖B,而B在运行时依赖C,那么A将在编译时依赖C。因此,减少编译时的依赖关系是非常重要的。

Elixir v1.11用 "exports dependencies "取代了 "struct dependencies"。换句话说,如果A依赖B,只要B的公共接口发生变化,A就会被重新编译。B的公共接口是由它的结构定义和它的所有公共函数和宏构成的。

这一变化允许我们将imports和requires标记为 "出口依赖",而不是 "编译时 "依赖。这极大地简化了依赖关系图。例如,在Hex.pm项目中,在Elixir v1.10中改变user.ex 文件会发出这样的信息。

$ touch lib/hexpm/accounts/user.ex && mix compile
Compiling 90 files (.ex)

在Elixir v1.11中,我们现在得到。

$ touch lib/hexpm/accounts/user.ex && mix compile
Compiling 16 files (.ex)

为了使事情变得更好,Elixir v1.11还为伞状项目(以及一般的路径依赖)引入了更细化的跟踪。在以前的版本中,来自同级应用的模块总是被视为编译时的依赖。这往往意味着,改变一个应用程序将导致兄弟应用程序中的许多模块重新编译。Elixir v1.11将尽可能地把依赖的模块标记为出口,在这些情况下产生了巨大的改进。

为了完善编译器的增强功能,在Elixir v1.10中增加的--profile=time 选项现在也包括编译每个单独文件的时间。例如,在Plug项目中,人们现在可以得到。

[profile] lib/plug/conn.ex compiled in 935ms
[profile] lib/plug/ssl.ex compiled in 147ms (plus 744ms waiting)
[profile] lib/plug/static.ex compiled in 238ms (plus 654ms waiting)
[profile] lib/plug/csrf_protection.ex compiled in 237ms (plus 790ms waiting)
[profile] lib/plug/debugger.ex compiled in 719ms (plus 947ms waiting)
[profile] Finished compilation cycle of 60 modules in 1802ms
[profile] Finished group pass check of 60 modules in 75ms

在实现这些功能的同时,我们也使--long-compilation-threshold 标志更加精确。在以前的版本中,--long-compilation-threshold 会同时考虑一个文件的编译时间和其他文件的等待时间。在Elixir v1.11中,它只考虑编译时间。这意味着更少的误报,现在你可以有效地获得所有编译时间超过2s的文件,在执行时间上,通过--long-compilation-threshold 2

config/runtime.exsmix app.config

Elixir v1.9引入了一个新的配置文件,名为config/releases.exs 。然而,这个新的配置文件只在发布期间执行。对于那些不熟悉发布的人来说,发布是一个包含了Erlang虚拟机、Elixir和你的应用程序的独立工件,可以在生产中运行。

这个新的配置文件被认为是对发布版非常有用的补充。因此,我们也引入了config/runtime.exs ,它在所有环境(开发、测试和生产)的代码编译后执行--适用于Mix和发布。我们的目标是为开发者提供一个更好的运行时配置体验,与我们目前的配置系统相比,它主要是以编译时为中心。

config/runtime.exs 与Elixir中任何其他配置文件的工作原理相同。然而,考虑到 是为了在生产系统中运行,而我们的 构建工具是不可用的,所以开发人员不得使用config/runtime.exs Mix Mix.env()Mix.target()config/runtime.exs 。相反,他们必须使用新的config_env()config_target() ,这两个文件已经被添加到模块中。 Config模块。

虽然config/releases.exs 将继续被支持,但开发者可以迁移到config/runtime.exs ,而不损失任何功能。例如,像这样一个config/releases.exs 的文件

# config/releases.exs
import Config

config :foo, ...
config :bar, ...

config/runtime.exs然而,鉴于config/runtime.exs 可以在所有环境中运行,你可能想把你的部分配置限制在:prod 环境中。

# config/runtime.exs
import Config

if config_env() == :prod do
  config :foo, ...
  config :bar, ...
end

如果这两个文件都是可用的,版本将选择现在首选的config/runtime.exs ,而不是config/releases.exs

为了总结这一切,Mix 还包括一个新的任务,叫做 mix app.config.这个任务加载所有的应用程序,并对它们进行配置,但不启动它们。每当你编写自己的混合任务时,你通常希望在运行自己的代码之前调用mix app.startmix app.config 。哪一个更好取决于你是想让你的应用程序运行还是只配置。

其他改进

Elixir v1.11增加了is_struct/2,is_exception/1, 和is_exception/2 守护。它还增加了对守护中的map.field 语法的支持。

日历模块提供了一个新的 Calendar.strftime/3函数,它提供了基于strftime 格式的日期时间格式化。该 Date模块有新的函数用于处理周和月,例如Date.beginning_of_month/1Date.end_of_week/2 。最后,所有的日历类型都有了与格里高利时间戳的转换函数,如Date.from_gregorian_days/2NaiveDateTime.to_gregorian_seconds/1

最后,为了使前几节所描述的编译器跟踪改进具有可见性,我们还在以下方面增加了新的功能 mix xref.mix xref 是一个描述项目中文件之间交叉引用的任务,对诊断项目中的大型编译周期很有帮助。

有关所有变化的完整列表,请参见完整的发布说明

查看安装部分以获得Elixir的安装,并阅读我们的入门指南以了解更多。

祝你玩得开心!