Elixir 1.11 + ライブラリ更新
Elixir v1.11
Elixir v1.11 released - The Elixir programming language
-
Elixir 1.10 のリリース後、チームはコンパイラの改善に取り組んできた。
- 今よりも多くのミスをコンパイル時に判定できるように
- 高速化
-
他の改善
- Erlang との連携強化
- さらに多くのガード式
- 組み込みの日時フォーマット
- カレンダー周りの強化
-
Logger
- レベルが増えて Erlang/OTP 21 の新しい
logger
や Syslog 標準に合うようになった - 構造化データ (map, keyword list) のロギングをサポート
- モジュールごとにログレベルを設定可能に
- レベルが増えて Erlang/OTP 21 の新しい
-
IEx で Erlang モジュールのドキュメントを表示できるようになった (Erlang/OTP 23+)
-
アプリケーション境界のチェック
-
Elixir v1.11 は v1.10 で追加された Compiler Tracer を使ってアプリケーション境界を追跡している。
-
Building Compile-time Tools With Elixir's Compiler Tracing Features | AppSignal Blog
-
たとえば v1.10 では、この機能によって、未定義関数の警告を個別に設定できるようになった。
-
@compile {:no_warn_undefined, OptionalDependency} defdelegate my_function_call(arg), to: OptionalDependency
-
-
これまでも Compile callbacks によってモジュールがコンパイルされるときに処理を追加することは可能だったが、Tracer によってコンパイラのイベントを処理するモジュールを実装できる。
-
あるアプリケーション内で、明示的に依存していないアプリケーションの関数を呼び出したときに警告を出すようになった。
- Elixir, Erlang/OTP のモジュールは常に利用可能なので、明示的に依存リストに加えていなくても使えてしまう
- Umbrella プロジェクトでも同様。同じ VM 上でコンパイルされるため、依存に加えていなくてもコンパイルできてしまう。
-
たとえば、scrivener_htm で以下のような警告が出た😅
-
warning: Plug.Conn.Query.encode/1 defined in application :plug is used by the current application but the current application does not depend on :plug. To fix this, you must do one of: 1. If :plug is part of Erlang/Elixir, you must include it under :extra_applications inside "def application" in your mix.exs 2. If :plug 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 :plug, you may optionally skip this warning by adding [xref: [exclude: [Plug.Conn.Query]]] to your "def project" in mix.exs lib/scrivener/html.ex:60: Scrivener.HTML.Default.path/3
-
Umbrella プロジェクト下の各アプリケーションで循環参照が生じていないかも検出できる。
-
Struct の存在しないフィールドのチェックでより多くの警告を出すようになった。
defmodule User do
defstruct name: nil, address: nil
def drive?(%User{age: age}), do: age >= 18
end
これをコンパイルすると Elixir 1.10 でも次のようなエラーが出る。
$ elixirc -v
Erlang/OTP 23 [erts-11.1.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]
Elixir 1.10.4 (compiled with Erlang/OTP 23)
$ elixirc user1.ex
== Compilation error in file user1.ex ==
** (CompileError) user1.ex:3: unknown key :age for struct User
user1.ex:3: (module)
Elixir 1.11 では次のパターンでも警告が出るようになった。
defmodule User do
defstruct name: nil, address: nil
def drive?(%User{} = user), do: user.age >= 18
end
これをコンパイルすると、Elixir 1.10 では何も問題なく実行できるが、
$ elixirc -v
Erlang/OTP 23 [erts-11.1.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]
Elixir 1.10.4 (compiled with Erlang/OTP 23)
$ elixirc user2.ex
Elixir 1.11 では次のようなエラーが出る。
$ elixirc -v
Erlang/OTP 23 [erts-11.1.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]
Elixir 1.11.3 (compiled with Erlang/OTP 23)
$ elixirc user2.ex
warning: redefining module User (current version loaded from Elixir.User.beam)
user2.ex:1
warning: undefined field "age" in expression:
# user2.ex:5
user.age
expected one of the following fields: __struct__, address, name
where "user" was given the type %User{} in:
# user2.ex:5
%User{} = user
Conflict found at
user2.ex:5: User.drive?/1
<<>>
演算子でのタイプチェック強化
Bitstring を構築する <<>>
演算子では、各セグメントのタイプチェックが強化された。たとえば、以下の run_length/1
関数にはバグがある。
defmodule Bits do
def run_length(bianry) when is_binary(bianry) do
<<byte_size(bianry)::32, bianry>>
end
end
<<>>
演算子で指定する各セグメントのタイプのデフォルトは integer
であり、バイナリ binary
を展開したい場合は明示的にタイプ ::binary
を指定する必要がある。
これも Elixir 1.10 では特に警告は出ないが、実行時に ArgumentError になる。
$ elixirc -v
Erlang/OTP 23 [erts-11.1.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]
Elixir 1.10.4 (compiled with Erlang/OTP 23)
$ elixirc bitstring.ex
$ iex
Erlang/OTP 23 [erts-11.1.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]
Interactive Elixir (1.10.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Bits.run_length("hi")
** (ArgumentError) argument error
bitstring.ex:3: Bits.run_length/1
Elixir 1.11 ではコンパイル時にチェックされるようになった。
$ elixirc -v
Erlang/OTP 23 [erts-11.1.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]
Elixir 1.11.3 (compiled with Erlang/OTP 23)
$ elixirc bitstring.ex
warning: incompatible types:
binary() !~ integer()
in expression:
# bitstring.ex:3
<<..., bianry>>
where "bianry" was given the type binary() in:
# bitstring.ex:2
is_binary(bianry)
where "bianry" was given the type integer() in:
# bitstring.ex:3
<<..., bianry>>
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
bitstring.ex:3: Bits.run_length/1
コンパイル時間の改善
再コンパイルが必要になるような依存関係の追跡について、Elixir 1.11 では多くの改善を施している。これまでのバージョンでは、以下の 3 種類の依存関係を追跡していた。
-
コンパイル時の依存関係 (compile time dependencies) - ファイル
A
がファイルB
にコンパイル時に依存している(A
がB
で定義されているマクロを使用しているなど)。B
を変更した場合、A
が再コンパイルされる。 -
struct の依存関係 (struct dependencies) - ファイル
A
がファイルB
に定義されているstruct
を使用している。B
内のstruct
の定義を変更した場合、A
が再コンパイルされる。 -
実行時の依存関係 (runtime dependencies) - ファイル
A
がファイルB
に実行時に依存している場合、B
を変更してもA
は再コンパイルされない。
Elixir 1.11 では、上記の struct dependencies を export dependencies に拡張した。Exports dependencies については mix xref のページの Dependencies types に詳しく説明されている。
Exports dependencies are compile time dependencies on the module API, namely structs and its public definitions. For example, if you import a module but only use its functions, it is an export dependency. If you use a struct, it is an export dependency too. Export dependencies are only recompiled if the module API changes. Note, however, that compile time dependencies have higher precedence than exports. Therefore if you import a module and use its macros, it is a compile time dependency.
これによって、import
や require
が compile time dependencies から export dependencies にすることができた。具体例を見た方が分かりやすいと思うので、以下のふたつのモジュールを考える。
defmodule ElixirV11.ModuleA do
alias ElixirV11.ModuleB
import ModuleB, only: [fetch_name: 1]
def hello(%ModuleB{} = m) do
case fetch_name(m) do
{:ok, name} ->
IO.puts("Hello, #{name}!")
:error ->
IO.puts("Who?")
end
end
end
defmodule ElixirV11.ModuleB do
defstruct name: nil
def new(name), do: %__MODULE__{name: name}
def fetch_name(%__MODULE__{name: nil}), do: :error
def fetch_name(%__MODULE__{name: name}), do: {:ok, name}
end
moduleA.ex
の import ModuleB, only: [fetch_name: 1]
は、Elixir 1.10 では compile time dependencies だったが、Elixir 1.11 では export dependencies になっている。
そのため、moduleB.ex
を変更したときに、moduleA.ex
の再コンパイルが不要である。
# Elixir 1.10
$ touch lib/moduleB.ex
$ mix compile --verbose
Compiling 1 file (.ex)
Compiled lib/moduleB.ex
Compiled lib/moduleA.ex
# Elixir 1.11
$ touch lib/moduleB.ex
$ mix compile --verbose
Compiling 1 file (.ex)
Compiled lib/moduleB.ex
touch
だけでは効果が分かりづらいので、公開インターフェースを変えない範囲で ModuleB
の実装を変えてみよう。
defmodule ElixirV11.ModuleB do
defstruct name: 1
def new(name), do: %__MODULE__{name: check_name!(name)}
def fetch_name(%__MODULE__{name: nil}), do: :error
def fetch_name(%__MODULE__{name: name}), do: {:ok, name}
defp check_name!(%__MODULE__{name: name}) when is_binary(name), do: name
end
公開インターフェースに変更がないので、これも再コンパイルは不要である。
# Elixir 1.10
$ mix compile --verbose
Compiling 1 file (.ex)
Compiled lib/moduleB.ex
Compiled lib/moduleA.ex
# Elixir 1.11
$ mix compile --verbose
Compiling 1 file (.ex)
Compiled lib/moduleB.ex
他の変更
- 重複した @doc で警告が出るようになった。調査したときのスクラップ
TBD
kiex で Elixir 1.11 をインストール
$ kiex install 1.11.3
$ kiex use 1.11.3
$ kiex default 1.11.3
ビルド結果など全て削除
$ rm -rf _build deps
deps を全てアップデート
$ mix deps.update --all
改めてコンパイル
$ mix deps.get
$ mix compile
Apple M1 + Big Sur のせいなのか以下のエラーが出る。
Compiling 22 files (.ex)
could not compile dependency :ex_doc, "mix compile" failed. You can recompile this dependency with "mix deps.compile ex_doc", update it with "mix deps.update ex_doc" or clean it with "mix deps.clean ex_doc"
** (File.Error) could not write to file "/Users/takanori.ishikawa/Developer/Workspace/myapp/_build/dev/lib/ex_doc/ebin/ex_doc.app": no such file or directory
(elixir 1.10.3) lib/file.ex:1050: File.write!/3
(mix 1.10.3) lib/mix/tasks/compile.app.ex:165: Mix.Tasks.Compile.App.run/1
(mix 1.10.3) lib/mix/task.ex:330: Mix.Task.run_task/3
(mix 1.10.3) lib/mix/tasks/compile.all.ex:76: Mix.Tasks.Compile.All.run_compiler/2
(mix 1.10.3) lib/mix/tasks/compile.all.ex:56: Mix.Tasks.Compile.All.do_compile/4
(mix 1.10.3) lib/mix/tasks/compile.all.ex:27: anonymous fn/2 in Mix.Tasks.Compile.All.run/1
(mix 1.10.3) lib/mix/tasks/compile.all.ex:43: Mix.Tasks.Compile.All.with_logger_app/2
(mix 1.10.3) lib/mix/task.ex:330: Mix.Task.run_task/3
** (exit) 1
(mix 1.10.3) lib/mix/tasks/cmd.ex:45: Mix.Tasks.Cmd.run/1
(mix 1.10.3) lib/mix/task.ex:330: Mix.Task.run_task/3
(mix 1.10.3) lib/mix/project.ex:352: Mix.Project.in_project/4
(elixir 1.10.3) lib/file.ex:1544: File.cd!/2
(mix 1.10.3) lib/mix/task.ex:430: anonymous fn/4 in Mix.Task.recur/1
(elixir 1.10.3) lib/enum.ex:2111: Enum."-reduce/3-lists^foldl/2-0-"/3
(mix 1.10.3) lib/mix/task.ex:429: Mix.Task.recur/1
(mix 1.10.3) lib/mix/project_stack.ex:181: Mix.ProjectStack.recur/1
出る箇所はランダムなので、File API が壊れてる?
コンパイルエラー
warning: undefined module attribute @action_fallback, please remove access to @action_fallback or explicitly set it before access
lib/myapp/controllers/async_api/result_store_controller.ex:40: MyApp.AsyncAPI.ResultStoreController (module)
== Compilation error in file lib/myapp/controllers/async_api/result_store_controller.ex ==
** (CompileError) lib/myapp/controllers/async_api/result_store_controller.ex:1: undefined function nil/2
(elixir 1.10.3) src/elixir_locals.erl:114: anonymous fn/3 in :elixir_locals.ensure_no_undefined_local/3
(stdlib 3.14) erl_eval.erl:680: :erl_eval.do_apply/6
(elixir 1.10.3) lib/kernel/parallel_compiler.ex:304: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7
undefined module attribute
警告は次のようなコード
@action_fallback MyApp.FallbackController
action_fallback @action_fallback
action_fallback/1
内で Macro.expand/2 するようになっている。[1]
Macro.expand/2 は module attributes も展開するはずだが... [2]
The following contents are expanded:
- Macros (local or remote)
- Aliases are expanded (if possible) and return atoms
- Compilation environment macros (
__CALLER__/0
,__DIR__/0
,__ENV__/0
and__MODULE__/0
)- Module attributes reader (
@foo
)
該当箇所で inspect してみる。
warning: undefined module attribute @action_fallback, please remove access to @action_fallback or explicitly set it before access
lib/abeja_dap_gateway_web/controllers/async_api/result_store_controller.ex:40: AbejaDapGatewayWeb.AsyncAPI.ResultStoreController (module)
#Macro.Env<
aliases: [
{Cached, AbejaDapBase.Cached},
{AsyncAPI, AbejaDapBase.AsyncAPI},
{ApplicationInstance, AbejaDapBase.ApplicationInstance},
{BodyParser, AbejaDapGatewayWeb.Plug.BodyParser},
{PlanActivationRequired, AbejaDapGatewayWeb.Plug.PlanActivationRequired}
],
context: nil,
context_modules: [AbejaDapGatewayWeb.AsyncAPI.ResultStoreController],
file: "/Users/ishikawasonkyou/Developer/Workspace/arms/apps/abeja_dap_gateway/lib/abeja_dap_gateway_web/controllers/async_api/result_store_controller.ex",
function: nil,
functions: [
{AbejaDapBase.ControllerHelpers,
[parse_integer_param: 2, parse_string_param: 2]},
{AbejaDapBase.Plug.Conn, [fetch_organization_id: 1]},
{AbejaDapGatewayWeb.Router.Helpers, [async_api_result_store_path: 5, ...]},
{Plug.Conn, [...]},
{Phoenix.Controller.Pipeline, ...},
{...},
...
],
lexical_tracker: #PID<0.426.0>,
line: 40,
macro_aliases: [],
macros: [{Phoenix.Controller.Pipeline, ...}, {...}, ...],
module: AbejaDapGatewayWeb.AsyncAPI.ResultStoreController,
requires: [...],
...
>
nil
このへんの話か。
That’s a useful distinction, but I don’t understand why code outside the quote is evaluated at compile-time while code inside it is evaluated at runtime.
Because code inside a quote is not run, the AST of the code is returned instead:
...
So when your module returns the quote’s return, it is just returning AST, which is then just put in place in the module at the location of the module call, it is not executed until runtime as it is just code at this point in AST form. :slight_smile:
やはり、コンパイルの挙動が不安定なので Erlang を Homebrew ではなくソースコードからインストールし直す。
brew uninstall elixir erlang
Uninstalling /usr/local/Cellar/elixir/1.11.3... (433 files, 6.1MB)
Uninstalling /usr/local/Cellar/erlang/23.2.1... (7,978 files, 460.7MB)
Github から clone してコンパイル
$ git clone https://github.com/erlang/otp.git
$ cd otp
$ git checkout OTP-23.2.1
$ ./otp_build autoconf
$ ./configure --with-ssl=/usr/local/opt/openssl
$ make
$ sudo make install
==> eex_html
Compiling 3 files (.ex)
== Compilation error in file lib/eex_html.ex ==
** (FunctionClauseError) no function clause matching in EExHTML.sigil_E/2
expanding macro: EExHTML.sigil_E/2
lib/eex_html.ex:255: EExHTML.javascript_variables/1
(elixir 1.11.3) lib/kernel/parallel_compiler.ex:314: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7
could not compile dependency :eex_html, "mix compile" failed. You can recompile this dependency with "mix deps.compile eex_html", update it with "mix deps.update eex_html" or clean it with "mix deps.clean eex_html"
** (exit) 1
(mix 1.11.3) lib/mix/tasks/cmd.ex:64: Mix.Tasks.Cmd.run/1
(mix 1.11.3) lib/mix/task.ex:394: Mix.Task.run_task/3
(mix 1.11.3) lib/mix/project.ex:352: Mix.Project.in_project/4
(elixir 1.11.3) lib/file.ex:1553: File.cd!/2
(mix 1.11.3) lib/mix/task.ex:511: anonymous fn/4 in Mix.Task.recur/1
(elixir 1.11.3) lib/enum.ex:2193: Enum."-reduce/3-lists^foldl/2-0-"/3
(mix 1.11.3) lib/mix/task.ex:510: Mix.Task.recur/1
(mix 1.11.3) lib/mix/project_stack.ex:181: Mix.ProjectStack.recur/1
ううむ、これは sigil_E
が重複してるのかな...
$ ag sigil_E deps
deps/scrivener_html/README.md
141:# Using Phoenix.HTML's sigil_E for EEx
deps/phoenix_html/lib/phoenix_html.ex
93: defmacro sigil_E(expr, opts) do
deps/eex_html/lib/eex_html.ex
139: defmacro sigil_E({:<<>>, [line: line], [template]}, []) do
依存してるのは Raxx か...。
依存ライブラリを取り除く
- mix.exs から削除
mix deps.unlock --unused
アップデートしてテストを実行したところ、debug ログが出力されているように見える。
- 関係ありそう Can't configure logger in ExUnit tests run with Mix, in Elixir 1.11 - Questions / Help - Elixir Programming Language Forum
- これ読んで、まさかログレベルから
:warn
がなくなったのかと思って、:warning
に変えてみたけど効果なし https://elixirforum.com/t/when-how-is-logger-app-started/36382/3 - これっぽい Logger level from
config.exs
is not respected in some cases · Issue #10589 · elixir-lang/elixir
mix.exs を見直してみたら、こうなっていた。--no-start
が原因だ...。
defp aliases do
[
...
test: ["ecto.create --quiet", "ecto.migrate", "test --no-start"]
]
end
この設定はどこから持ってきたのだろう...。
以下のようなコードで確認
IO.puts("Logger.level() = #{Logger.level()}, config = #{Application.get_all_env(:logger)[:level]}")
Elixir 1.11.3 + --no-start
Logger.level() = debug, config = warn
Elixir 1.11.3
Logger.level() = warning, config = warn
config が warn
でも warning
に変換される
Elixir 1.10.3 w/ or w/o --no-start
Logger.level() = warn, config = warn
Logger.level()
も :warn
Elixir 1.10 と Elixir 1.11 での比較
Elixir version | config | Logger.level() |
---|---|---|
Elixir 1.10.4 | :warn |
:warn |
Elixir 1.10.4 (--no-start) | :warn |
:warn |
Elixir 1.11.0 | :warn |
:warning |
Elixir 1.11.0 (--no-start) | :warn |
:debug |
1.11 の CHANGELOG には "Soft-deprecations (no warnings emitted)" として warn
ログレベルの非推奨が記載されている。
[Logger]
warn
log level is deprecated in favor ofwarning
Umbrella プロジェクトのトップレベルで iex を起動するとエラー
warning: redefining @doc attribute previously set at line 161.
Please remove the duplicate docs. If instead you want to override a previously defined @doc, attach the @doc attribute to a function head:
@doc """
new docs
"""
def lookup_trial(...)
@doc
が重複していたら警告が出るようになった。[1] 上書きしたい場合は、関数の本体を書かない function head で書く。
Ecto.Migration のテスト
Ecto 3.17 から 3.2.2 にアップデート: Migration のテスト - Qiita でも書いた通り、
もっとも、Ecto の実装が変わるたびに書き換えが必要になるのはメンテナンス性が悪い(書き換えるのも二度目)。ecto_sql の integration test のように書いた方がいいかもしれない。
3.5 にアップデートするときも辛かったので、覚悟を決めて書き直す。
- 最新バージョンでもテスト方法は変わっていない
- 公開モジュールである Ecto.Migrator を使ってテストを書く
- Migration モジュールを個別で up/down できる
- SQL Sandbox を auto にしておく(トランザクションは使用しない)
Ecto.Adapters.SQL.Sandbox.mode(MasterRepo, :auto)
- 作成されたテーブルに対して SQL を実行して調べる
- Repo.query や Repo.all を使う
- PostgreSQL でカラムの情報を取得するには information_schema.columns
information_schema にクエリを発行するときは prefix が必要
MasterRepo.all(
from(i in "columns",
select: [i.column_name, i.data_type],
where: i.table_name == "create_table_migration"
),
prefix: "information_schema"
)
このまま実行すると以下のような警告が出る。
[warn] You are running migration 1000419 but an older migration with version 20200721175245 has already run.
This can be an issue if you have already ran 20200721175245 in production because a new deployment may migrate 1000419 but a rollback command would revert 20200721175245 instead of 1000419.
If this can be an issue, we recommend to rollback 1000419 and change it to a version later than 20200721175245.
すでにアプリケーション本体の Migration で 20200721175245
などの version で実行しているためで、この警告に対処するためには、
- そのまま無視する
-
@moduletag :capture_log
でログを出さないようにする (Ecto.Integration.MigrationTest
はこれ) -
Ecto.Migrator.up/4
に渡すversion
をアプリケーション本体の Migration よりも新しいものにする
肌感として unused variables をいっぱい拾うようになった。