世界を動かす技術を、日本語で。

Elixir v1.20: 段階的型付け言語になりました

概要

  • Elixir v1.20 で新しい型システムの開発マイルストーンを達成
  • 型推論と漸進的型チェック を型アノテーションなしで実現
  • dynamic()型 により既存コードのバグやデッドコードを自動検出
  • 型システムの目標 ・dynamic()型の特徴・今後の展望を解説
  • コンパイル高速化 や今後の型シグネチャ導入計画も紹介

Elixir型システムの進化とdynamic()型の役割

  • 2022年、 set-theoretic types 導入の取り組みを発表
  • 2023年6月、 Elixirの型システム設計論文 を発表し、研究から開発フェーズへ移行
  • Elixir v1.20 で、型アノテーション不要の型推論と漸進的型チェックを実装
    • 既存コードの デッドコード検証済みバグ (実行時に確実に失敗する型違反)を自動検出
    • 開発者の負担増なし極めて低い誤検出率
  • dynamic()型 の導入
    • 他言語のany()型と異なり、 互換性(compatibility)絞り込み(narrowing) の性質を持つ
    • 型がdisjoint(交わらない)場合のみ型違反を報告し、 誤検出を最小化
    • 型情報を保持しつつ、コードの流れで型を絞り込む ことで、実用的なバグ検出を実現

dynamic()型による型推論とバグ検出

  • dynamic()型は 実行時に型が決まる 変数や式に付与される
  • 例:
    • value_or_error = if value > 1, do: value, else: "not well"
      • 型はdynamic(integer() or binary())
      • /演算子やString.upcaseの呼び出し時、型が交わるため違反を報告しない
    • Map.fetch!(value_or_error, :some_key)
      • 受け入れ型がmap、渡す型がinteger/binaryで disjoint なため型違反を検出
  • 型の絞り込み(narrowing)
    • 例:def add_a_and_b(data), do: data.a + data.b
      • dataは最初dynamic()型だが、フィールドアクセスで%{..., a: number(), b: number()}型に絞り込み
      • 誤った記述(data.a + dataなど)で型違反を適切に検出
  • 他言語のgradual type systemと異なり、 dynamic()型でも型情報を保持し続ける ため、精度の高いバグ検出が可能

型ガード・条件分岐・標準ライブラリ関数の型推論

  • ガード節 での型推論
    • when is_list(x) and is_integer(y) → xはlist、yはinteger
    • when is_binary(x) or is_integer(x) → xはbinaryまたはinteger
    • when is_map_key(x, :foo) → xは:fooキーを持つmap
    • when not is_map_key(x, :foo) → xは:fooキーを持たないmap
  • データ構造のサイズ判定
    • when tuple_size(x) < 3 → xは2要素以下のtuple
    • listsやmapsも 空判定 に変換し型推論
  • case文や条件分岐 での型の絞り込み
    • 例:case System.get_env("SOME_VAR") do ... end
      • nilとbinary()を適切に分岐し、後続の型違反を防止
    • 不要な分岐やデッドコードの検出 にも寄与
  • 標準ライブラリ関数 (tuples, maps等)も型付けを強化

コンパイル時間の改善と新オプション

  • Elixir v1.20 はマルチコア環境での コンパイル速度 をさらに向上
    • BEAM系言語で最速レベルのビルドツール
  • 新しいコンパイラオプション:module_definition
    • :compiled(デフォルト)または:interpretedを選択可能
    • 大規模プロジェクトでの ビルド時間短縮 に寄与
    • mix.exselixirc_options: [module_definition: :interpreted]を設定

今後の展望と型シグネチャ導入への課題

  • 型シグネチャ(型注釈)導入は未定
    • v1.20の型システム性能に満足できるか
    • 再帰型パラメトリック型mapのkey-value走査 の効率的実装が前提
  • これらの課題解決後、 typed struct定義や型シグネチャ の検討・導入を開始予定
  • 最新情報はElixir Forumや公式ニュースで随時発信

まとめ・コミュニティへの呼びかけ

  • Elixir v1.20 で型推論・型チェックの新しい体験を提供
  • 既存コードのバグ検出・デッドコード排除を 自動化し開発効率を向上
  • リリース候補のテスト・フィードバック・ベンチマーク提供にコミュニティへ感謝
  • Elixir v1.20を試し、検出されたバグを修正しよう

Hackerたちの意見

やばい、また一年間エリクサーを学ぶことになりそう。エリクサーのことは全部好きなんだけど、他の言語とは違って、自分に自信を持てなくさせるんだよね。俺の脳みそは関数型のことには向いてないけど、また挑戦したくなる。初心者には優しくないエコシステムなのが残念で、質問しても、みんながもう言語についてたくさん知ってる前提で答えることが多いんだよね。

エリクサーフォーラムで質問してみるといいよ。敵意のある反応を見たことはないな。時々、あいまいな投稿は反響がないことがあるし、「宿題やって」みたいな匂いがする投稿は無視されることもあるけど、純粋な好奇心がある投稿には、ちゃんと答えてもらえると思うよ。

コミュニティはすごく優しいから、きっと助けてもらえるよ。

どんな関数型のことが引っかかってるの?始めたばかりの頃は、たくさんのことを手続き型で書けるからね。

もしかして、Rustを少し知ってる?俺もFP言語にはあんまり経験がないけど、GleamはRustの影響で結構馴染みがあって、構文よりも概念に集中できたんだ。確かに、数回午後を使ったけど、もしもう一度FP言語を選ぶなら、Gleamにすると思う。親しみやすさがあるからね。

https://pragprog.com/titles/lhelph/functional-web-developmen... タイトルに騙されないでね - 本の前半はエリクサーの過去8年間のことが書いてある。この本を使ってエリクサーを再学習してるけど、毎回うまくいくんだ。俺は一度も最後まで読んだことがないけど、良いプログラミング本の証拠として、何度も始めては途中で終わっちゃうんだ。終わる前に、自分のことをやるための道具を手に入れちゃうからね。

初心者にはこのリソースがよく響くと思うよ: https://joyofelixir.com/toc.html

こういうコメントはいつも混乱する。状態が多いオブジェクト指向プログラムは、私には理解するのがずっと難しいから。

https://htdp.org をやって、すべての演習を注意深くやってみて(最初は赤ちゃんの作業みたいに感じるかもしれないけど) - 機能型のことに頭を再トレーニングすることになるよ。:-)

Elixirのすべてが大好きだけど、Elixirは他の言語とは違って常に自分を疑わせるんだ。私の脳は関数型のことには向いてないけど、もう一度挑戦したくなる。大学の時に「プログラミングパラダイムの調査」みたいなコースを受けて、初めてHaskellを試したときに本当に痛い思いをした。あの時まで何年もプログラミングしてたのに、ずっと「基本的」だと思ってたことを完成させるのがこんなに無力だとは信じられなかった。でも、脳が向いてないわけじゃなくて、命令型言語での経験レベルとの対比だと思う。純粋な関数型スタイルで作業すると、また初心者から始めることになるんだ。徐々に上達すると思うよ。私が関数型プログラミングを快適に感じるようになったのは、基本的にもっと余裕のあるBashの「ワンライナー」のようにコードを組み合わせるのが好きだと気づいたからだと思う。データは最初にある形から始まって、コマンドを実行してそれをダンプする。次に、欲しいものに近づけるステップを考えて、それを次のコマンドにパイプして、もう一度見直す。そうやって続けていくと、最終的に見るものは、決して変えないデータの変換の連続にかなり近いものになる!シェルでこれが快適に感じるのは、毎日ファイルシステムをいじってるだけでそのコマンドの語彙が増えていくからだよ。年月が経つにつれて、Unixライクな環境での「関数」のライブラリがかなり大きくなった。純粋な関数型プログラミング環境でも同じことをしなきゃいけないけど、語彙を学ぶのにちょっと努力が必要なんだ。よく使う「コマンド」はgrepやcat、sortの代わりにmapやfold、zipみたいな関数になる。でも、核心は本当に同じで、パイプラインを構築する楽しさはどちらにも当てはまるんだ:少しずつ作り上げていけるし、パズルに取り組んでいるときは、前のステップを忘れて目の前のデータの次の変換だけを考えればいい。これがすごくリフレッシュできて、リラックスできる低コンテキストな感じがするんだ。とにかく、試してみて楽しんでほしいな。何かが苦手なことを楽しめるようになったとき、それが結局上達する方法なんだ。

HNでエリクサーの漸進的型システムについての投稿をいくつか見たけど、あんまり詳しくは追ってないんだ。誰か、この特定の漸進的型システムが、未型付きコードに対してプログラムの漸近的な性質を変えられるか知ってる人いる?俺の知る限り、ほとんどの漸進的型システム(例えばRacket)は、プログラムを漸近的に遅くすることがあるけど、例外もあるみたいだね。[1]

エリクサーの漸進的型システムは、プログラムの漸近的な複雑さを変えることはできないよ。デザイン上、他の漸進的型システムでの遅延を引き起こすメカニズム(静的/動的境界でのランタイムキャスト)を明示的に排除してるんだ。ほとんどの漸進的型システムは、値が型/未型の境界を越えるときに強制変換を挿入するけど(リストの各要素をチェックしたり、値を型付きプロキシでラップしたりする)、エリクサーチームは、ランタイムチェックなしでの健全性を達成するために「強い矢印」の結果を発表したんだ。コンパイラが出力するバイトコードは、未型付きコードと意味的に同じだよ。

これいいね!1.20は大きな傘アプリをかなり早くコンパイルしてるみたい。

ElixirとPhoenixは面白いと思ったけど、また置いておいた理由が2つあるんだ。まず、BeamとElixirが必要ってこと。これ、ちょっと変だなと思う。PythonやJava、C、Rustみたいに言語だけでいいのに、なんでその下に何かが必要なんだろう。デバッガーもないし、Elixirをデバッグする方法はコンソールに出力することだけ。40年前みたいだよ。いらないな。

JavaにはJVMがあるのと同じように、ElixirにはBeam/OTPがあるんだよね。

それは間違ってる。> BeamとElixirが必要。これ、ちょっと変だなと思う。PythonやJava、C、Rustみたいに言語だけでいいのに、なんでその下に何かが必要なんだろう。BeamはVMだよ。JavaもVMが必要だって分かるよね?JVMって呼ばれる理由があるんだから。それにPythonもインタプリタが必要だし。> デバッガーはない。Elixirをデバッグする方法はコンソールに出力することだけ。40年前みたいだ。それは間違いだよ。https://www.erlang.org/doc/apps/debugger/debugger_chapter.ht... それにオブザーバーもあるし、他にもたくさんのデバッグツールがあるよ。Javaにはいいデバッガーがあるって聞くけど、もしかしたらそれがいいのかも(使ったことないけど)。でもBeamにデバッガーがないってのは本当じゃないよ。

本当にその笑いが必要だった。ありがとう!

確かに、プリントデバッグだけじゃないよね。red(x)bugみたいなツールも使えるし、Elixir-LSプロジェクトはデバッグアダプタープロトコルに対応してる。個人的には、REPL(とちゃんとしたソフトウェアアーキテクチャ)があるおかげで、必要に応じて関数を実行するだけでコードを調べやすいと思うよ(本番環境でもね)。

Javaに慣れてるなら、Elixirはjavacみたいなもので、Beamはjavaみたいな感じ。Mixは(ずっと良い)Gradleのバージョンみたいなもの。アプリをコンパイルするにはElixirが必要だけど、実行するにはBeamだけで大丈夫。プロジェクトをビルドしたら、Javaやjavacと同じようにElixirはもう必要ない。CやRustはマシンコードにコンパイルされるからランタイム依存はないけど、ビルド時にはコンパイラが必要なのはElixirと同じだね。

素晴らしいね。ダイナミックタイピングで悪い経験をしたせいでElixirを敬遠してる開発者を何人か知ってる。これが役に立つといいな!

Gleamと比べてどうなの?それとも、今Elixirを使う理由は何?特にPhoenixやLive ViewがElixirの大きな魅力だと思うけど。

Rustが好き?それともErlangが好き?Gleamを書くのはRustを書くのに似てて、Elixirを書くのはErlangを書くのに似てる。今のGleam OTPの状態は知らないけど、前に見たときはあんまり良くなかった。もしその2つにこだわらず、型だけが気になるならGleamを使えばいい。でも、だったらRustを使えばいいじゃん?

Gleamのウェブサイトをチェックしてみて、比較がちゃんと載ってるよ。

Gleamにはマクロがないから、PhoenixやEctoみたいな多くのElixirライブラリが効果的に使ってるものが使えないんだ。例えば、GleamはJSONのデコード/エンコードの冗長性に問題があるけど、Rustではserdeを使って、Elixirでは関数呼び出し一つで済むんだ。Elixirのエコシステムはもっと成熟してるよ。Gleamを使ってPhoenixや他のGleamフレームワークを使うこともできるけど、体験は全然違うんだ。Gleamの魅力は型付け(Elixirもそのギャップを埋めつつあるけど)とJavaScriptにコンパイルできること(HologramもElixirのためにやってることだね)。私はGleamの型システムとRustっぽい構文が好きだけど、今のところはElixirがウェブ開発プロジェクトにはベストな選択だと思ってる。

Elixirをアップデートするのはすごくいいね。いろんなプロジェクトで破壊的変更がなくて、コンパイラがバグを自動で見つけてくれる。ほんと贅沢だわ。

みんな、ここでの損失については申し訳ない。def example(x) when not is_map_key(x, :foo) これも、単にRubyの構文をコピー&ペーストするだけじゃ勝ちじゃないってことを示してると思う。クリスタルでも前に気づいたけど、クリスタルは最初から型があったからね。基本的には、def foo() endはシンプルであるべきだと思う。今はもうそうじゃないけど。(Rubyも「無限メソッド」みたいなエラーを起こしたし、プログラミング言語がここ5年くらいでおかしな方向に行くのが理解できない。)

もちろん、2つ目のこともできるよ。型は強制されないから、必要なければ使わなくてもいい!

君がコメントしてる構文は、Elixirのv1.0以前からパターンやガードの一部として存在してたんだよ。今になって追加されたみたいに言ってるけど、言語の表面には何の変更も加えてないんだ。違いは、今は同じ言語構造を使って、正確な型情報を抽出できるようになったことだね。

これを見るとすごく嬉しい!「素晴らしい言語」レベルに近づいてると思うし、私にとってはこれが初めてだよ。他に信頼性があって安全に素晴らしい機能を追加して、使いやすい言語があったら教えてほしいな。私はGoをマスターした後、C#の上級を学び始めたんだけど、Goが素晴らしいものを追加するのをやめちゃったから :(

批判者たちが「型付けが必要だ」って言ってたのが皮肉だよね。Elixirファンは「型付けなんていらない、Elixirは魔法みたいだからバグは出ない」って言ってたのに、今は型付けがあってバグを見つけてるんだから…でも、バグを防ぐのにそれが必要ないって言ってたよね?でも、良いことだね!少し前にElixirを試してみたけど、楽しかったけど型がないのには同意できなかったな。