Elixir v1.14 リリース記事の感想やメモ

v1.14 リリース*1

Rust の dbg! は理に適ってて羨ましいなと思っていたところやってくれた。 単なる真似では終わらせないという José Valim の心意気にしびれる。

--以下感想--

ElixirConf 2022 - José Valim - Elixir v1.14 - YouTube

これまでのリリースと同様、開発者体験に焦点を置き、デバッグ作業の改善、 デバッグツール、評価式の調査出力の改善を施した

dbg

Kernel.dbg/2IO.inspect/2 に似たマクロで、デバッグ作業用に特別にしつらえたやつである

IO.inspect/2 を置き換えるように使えて、デバッグコード自身とコードの位置、引数で与えた内容を要素ハイライト付き(!)で印字する

dbg/2 はさらに、マクロであることから Elixir コードを理解する
|> による連続パイプを渡すとそれらすべてのステップを印字する

IEx + dbg

IEx シェルでのブレイクポイントで行ごとのステップ進行が可能になった

iex> break! URI.parse/1

で、URI.parse 関数の先頭にブレイクポイントを置くと

iex> URI.parse "/foo"

デバッグモードに入り、現在行までに評価された変数を確認でき、n で一行ステップを進められるようになった

Break reached: URI.parse/1 (lib/elixir/lib/uri.ex:770)
                                                              
  767:   def parse(string) when is_binary(string) do
  768:     # From https://tools.ietf.org/html/rfc3986#appendix-B
  769:     # Parts:    12                        3  4          5       6  7        8 9
  770:     regex = ~r{^(([a-z][a-z0-9\+\-\.]*):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?}i
  771:
  772:     parts = Regex.run(regex, string)
  773:

pry(1)> n
...
pry(2)> n
...

これは dbg/2 でも利用できる
dbg/2 は構成可能なバックエンドをサポートする。
IEx は自動で既定のバックエンドを dbg/2 のバックエンドに交換し、
それが IEx 上でコード実行を停止させる

我々はこのプロセスを "prying"(=覗き見)と呼ぶ
なぜなら変数やインポートへのアクセスは得るが、コードの実際の動きは変更できないようになっているから

これはパイプライン上でも機能する
連続した |> パイプを dbg へ渡す(または末尾に |> dbg() をつなぐ)と、すべてのパイプを一行ずつステップすることが可能

既定では iex 上で dbg() を実行すると pry プロセス移行の Allow? [Yn] プロンプトが現れるが、--no-pry フラグをつけて実行するとモード移行を省略できる

dbg in Livebook

Livebook により Jupyter Notebook のような計算ノートの能力が Elixir にやってくる

Livebook チームは dbg のバックエンドとして視覚化された表現形式を実装した。 こいつのおかげでパイプラインステップの1つ1つが単独で機能するUI要素として描画される。 要素を一つ選び取って出力を見ることもできるし、さらに、パイプラインを並べ替えたり 一定区間を無効化したりしてその結果を即座に見られる

PartitionSupervisor

PartitionSupervisor は新しいタイプのスーパーバイザを実装する
単体の管理(supervised)プロセスがボトルネックになってくる状況で役立つようデザインされている

管理プロセスの状態が容易に区分可能である場合、PartitionSupervisor によってそのプロセスのコピーを管理し、 複数の独立したプロセスとして同時並行に走らせ、各プロセスが自分用の区画を持つようにできる

たとえば ErrorReporter プロセスがあり、それを使って監視サービスにエラー報告したいとする

# Applicaiton supervisor:
children = [
    # ...,
    ErrorReporter
]

Supervisor.start_link(children, strategy: :one_for_one)

アプリの並行動作が増えていくにつれ ErrorReporter プロセスは多数の他プロセスから要求を受け取るかもしれず、 しまいにはボトルネックになりかねない。 このような状況では、ErrorReporter のコピーを PartitionSupervisor のもとで複数起ち上げるとよい

# Applicaiton supervisor:
children = [
    # ...,
    {PartitionSupervisor, child_spec: ErrorReporter, name: Reporters}
]

PartitionSupervisor は既定で System.schedulers_online() と同数のプロセス(たいていCPUコアにつき1つ)を起ち上げる。 これで ErrorReporter プロセスへの要求経路指定として :viaタプルを利用でき、区画スーパーバイザを経由して要求を渡せる

partitioning_key = self()
ErrorReporter.report({:via, PartitionSupervisor, {Reporters, partitioning_key}}, error)

この例のように self() を区画キーとして使うと同じプロセスのエラー報告先は常に同じ ErrorReporter プロセスとなり、背圧形式を保証できる。区画キーにはどんな評価式でも使用可能

一般的な例

PartitionSupervisor の一般的かつ実用的な好例は DynamicSupervisor のようなものを区画化することである

単体の動的スーパーバイザ配下で多数のプロセスを開始するとスーパーバイザがボトルネックになり得る。 とくにスーパーバイザ上でプロセス開始の初期化に時間がかかる場合など。 そこで、単体の DynamicSupervisor を起ち上げることはやめて、複数起ち上げてしまう:

children = [
    {PartitionSupervisor, child_spec: DynamicSupervisor, name: MyApp.DynamicSupervisors}
]

Supervisor.start_link(children, staragegy: :one_for_one)

これで適当な区画で複数の動的スーパーバイザを起ち上げられる。

例えば、先ほどの例のように PID で区画を切るなら:

DynamicSupervisor.start_child(
    {:via, PartitionSupervisor, {MyApp,DynamicSupervisors, self()}},
    my_child_specification
)

バイナリのエラーと評価の改善

Erlang/OTP 25 ではバイナリ構築時のエラーと評価を改善したが、これらの改善は Elixir にも適用された。

v1.14 より前はバイナリ構築時のエラーはしばしばデバッグしづらい漠然とした"引数エラー"に過ぎなかった

Erlang/OTP 25 と Elixir v1.14 ではデバッグ作業しやすいようさらなる詳細情報を提供する
この成果はEEP54の一部である

以前のこれが:

int = 1
bin = "foo"
int <> bin
#=> ** (ArgumentError) argument error

これからはこうだ:

int = 1
bin = "foo"
int <> bin
#=> ** (ArgumentError) construction of binary failed:
#=>    segment 1 of type 'binary':
#=>    expected a binary but got: 1

コード評価(IEx と Livebook)も同様に改善されてエラー報告とスタックトレースが良くなった

Slicing with Steps

Elixir v1.12 で導入された ステップ範囲 は "ステップ"(=小ジャンプ) を指定できる範囲である

Enum.to_list(1..10//3)
#=> [1, 4, 7, 10]

ステップ範囲は特にベクトルと行列が関わる数値計算で有用である(例えばNxを見よ ) ただ、Elixir 標準ライブラリは API にあまりステップ範囲を使ってこなかった。 Elixir v1.14 では手始めにいくつかの関数においてステップ範囲をサポートすることでステップを活用していくことにした。

その一つが Enum.slice/2 である:

letters = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
Enum.slice(letters, 0..5//2)
#=> ["a", "c", "e"]

binary_slice/2(厳密には binary_slice/3 も)が Kernel モジュールに追加され、バイナリに対して同様にステップ範囲をサポートする

binary_slice("Elixir", 1..5//2)
#=> "lxr"

Expression-based Inspection and Inspect Improvements

Elixir では不透明構造体を特殊な表記でもって調査出力するように Inspect プロトコルを実装する慣習があり、こんな具合である:

MapSet.new([:apple, :banana])
#MapSet<[:apple, :banana]>

この表記は一般的には構造体の内容またはその一部が非公開で %name{...} の表現形式を使うと公開 API 以外のフィールドが露出してしまう場合にそれを嫌って行われる

#name<...> 慣習表現が残念なのは調査出力が有効な Elixir コードではない点である。例えば、調査出力をコピーして IEx セッションに貼り付けても使えない

Elixir v1.14 ではいくつかの標準ライブラリの構造体についてこの慣習を改めた
それら構造体の Inspect プロトコルの実装はこれからは評価すればその構造体が再生成される有効な Elixir 式を文字列で返す。

上の MapSet の例では、こうなる:

fruits = MapSet.new([:apple, :banana])
MapSet.put(fruits, :pear)
#=> MapSet.new([:apple, :banana, :pear])

MapSet.new/1 式は調査出力している構造体そのものへと評価される。この式なら MapSetの内部構造を隠せるし、それでいて有効な Elixir コードのままである。この式形の調査出力は Version.Requirement, MapSet, Date.Range について実装済みである。

最後に、我々は構造体の Inspect プロトコルを改善し、構造体の調査出力のフィールドが defstruct において宣言された順になるように変更した

例えば URI 構造体は以前までは各フィールドがアルファベット順に表示されていたが、

iex> URI.new!("https://elixir-lang.org/")
%URI{
  fragment: nil,
  host: "elixir-lang.org",
  path: "/",
  port: 443,
  query: nil,
  scheme: "https",
  userinfo: nil
}

これからは URI 構造順に出力してくれる

iex> URI.new!("https://elixir-lang.org/")
%URI{
  scheme: "https",
  userinfo: nil,
  host: "elixir-lang.org",
  port: 443,
  path: "/",
  query: nil,
  fragment: nil
}

また、:optional オプションを追加して Inspect プロトコルを導出(deriving)する際に開発者が構造体表現についてより制御できるようにした

更新されたドキュメントで使い方の概略と利用可能なオプションについて見よ

Learn more

完全な変更リストについては、full release note を見よ

楽しいデバッグ作業を!

*1:日々のドキュメント作業に追われブログする暇など無くなった