Gleam はもう Erlang のソースコードにコンパイルされない

Gleam は以前、Erlang ソースコードにコンパイルして erlc に渡していた。バージョン 1.19 では直接 BEAM バイトコードを生成するようになった。この記事では、それによって得られた利点、なぜ BEAM アセンブリまで踏み込まなかったのか、そしてこの変更がもたらした最適化について説明する。

日本語
コピー

Gleam v1.19.0 リリース!

2026年10月5日、著者 Louis Pilfold

Gleam は Erlang 仮想マシンと JavaScript ランタイムを対象とした、型安全でスケーラブルな言語です。本日 Gleam v1.19.0 が正式にリリースされました。何が変わったのか見ていきましょう。

新しいコンパイルターゲット

この数か月、Giacomo Cavalieri が Gleam の Erlang コード生成器を全面的に書き直しました。新しい生成器は設計が根本から異なり、何より出力する形式が変わりました。以前は Erlang のソースコードを生成していましたが、今は Erlang abstract forms を生成します。

Erlang abstract forms は Erlang コンパイラが使う中間表現です。メタデータが付いた木構造で Erlang の構文を表し、通常は Erlang の字句解析器と構文解析器が生成します。Erlang の external term format に基づくバイナリエンコーディングがあり、これを使えば生成されたコードを直接ロードでき、Erlang コンパイラの前半部分を丸ごと飛ばせます。

新しい Erlang コード生成器にはいくつかの利点があります。

  • コンパイラの性能が向上し、Erlang 上で動く Gleam プロジェクトのビルド時間が大幅に短縮された。
  • 実行時に得られる位置メタデータが、コンパイラが生成するであろう Erlang ソースコードではなく、元の Gleam ソースコードに正確に対応するようになった。つまり BEAM のクラッシュレポートやスタックトレースの行番号が完全に正確で、以前のように不正確で直近の関数を指すだけ、ということがなくなった。このメタデータは edb のようなデバッガが Gleam を完全にサポートする道も開くが、その作業にはまだ手を付けていない。
  • Gleam コンパイラのコード品質が上がった。Erlang コード生成器は Gleam のコードベースで最も古く安定した部分の一つで、これまで問題を起こしたことはないものの、現在の基準や慣習には合っていなかった。新しい実装は非常に良くできており、コンパイラ全体の水準を引き上げていると言える。
  • 「transpiler」という言葉を蔑称として聞かされることも、もうなくなる。1

で、どれくらい速いのか?

数字は後で示しますが、ベンチマークは常に人為的なもので、決してすべてを語ることはできないと覚えておいてください。入門や出発点としては十分ですが、本当に理解するにはさらなる調査と実際の経験が要ります。

このベンチマークは José Valim の langcompilebench プロジェクトを基にしています。José に感謝します。100 個のモジュールをコンパイルするのにかかる時間を測るもので、各モジュールには "hello world" を返す関数が 100 個あります。この形のテストプロジェクトは言語間で簡単に複製でき、できるだけ対等なテストプロジェクトが得られるので実用的ですが、各言語のごく一部の機能しかコンパイル対象にならないため、分かることは限られます。実際のプロジェクトではコードの多様性ははるかに大きく、機能ごとのコンパイルコストも言語によって異なります。

コード生成器の書き直しの第一段階は v1.18.0(前のバージョン)でリリースされたので、v1.17.0 と今回の v1.19.0 を比べます。このグラフはベンチマークプロジェクトのコンパイルにかかる時間で、低いほど良いものです。

0ms100ms200ms300ms400ms500msGleam v1.19Gleam v1.17

ご覧のとおり、かなりの改善です!これはキャッシュなしの完全なフルビルドです。Gleam のコンパイルはインクリメンタルなので、日常の開発ではプロジェクト全体をコンパイルしないため、はるかに速くなります。

最初の langcompilebench には Erlang、Elixir、Gleam しか含まれていませんでしたが、他のさまざまな人気プログラミング言語も追加して、Gleam のコンパイル速度が皆さんの慣れた言語と比べてどうかをざっくり感じられるようにしました。JavaScript にコンパイルする Gleam も加えています。結果は次のとおりです。

0ms200ms400ms600ms800ms1000msGleam (JavaScript)GoGleam (Erlang)ErlangJavaElixirElmRustC#TypeScript 7

忘れないでください。これは人為的なベンチマークで、これだけでこれらの言語について断定的な結論を下すことはできません。とはいえ、Gleam のコンパイルがかなり速いことは確かに示していますし、Gleam プログラマとして言えば、Gleam での開発はとても快適で、コンピュータを待つ時間はほとんどありません。

BEAM バイトコードを直接生成しないのはなぜ?

Erlang コンパイラに渡すソースコードのコンパイルから、Erlang コンパイラに渡す中間表現のコンパイルへと移行しましたが、なぜ Erlang コンパイラを丸ごと迂回しないのでしょうか。Erlang コンパイラより優れた BEAM バイトコード生成器は作れないのでしょうか。Gleam の型情報を使って、より最適化されたコードを生成することもできるかもしれません。

そうした利点は実現可能かもしれませんが、実際にはまず実現しないでしょう。Erlang のソースコードや Erlang abstract forms と違い、BEAM バイトコードは固定されていません。仮想マシンが新バージョンを出すたびに、バイトコードは進化し、改善され、新機能が増え、時に不要になった機能が削除されることもあります。この進化についていくには、永遠に追い続ける覚悟が要ります。Erlang のメンテナと密に協力し、来るべき変更に先んじて備え、仮想マシンが新バージョンを出せば Gleam の新バージョンもそれに追随させる。しかも、Erlang コンパイラが何十年もかけて蓄積した最適化をすべて再現するのは、Gleam の強力な静的解析があっても膨大な作業です。

Gleam はスポンサーに支えられたコミュニティプロジェクトだ。企業や学術機関の後ろ盾がある言語と比べれば、資金は桁違いに少ない。だからこそ、限られたリソースをどう使えば最も効率的で持続可能なのか、慎重に考えなければならない。Gleam はソフトウェア開発における信頼できる基盤であり、下す決定の一つひとつが何年、何十年先まで通用するものでなければならない。Erlang abstract forms へのコンパイルは、現時点で Gleam にとって最も費用対効果の高い選択だ。

この決定には心強い先例もある。我らが敬愛する兄貴分 Elixir も、同じく abstract forms 経由で Erlang にコンパイルしている。Elixir で通用するなら、Gleam でも通用する。

さて、この話はここまで。Gleam v1.19.0 にはまだまだ見どころがある。

JavaScript の決定木代入の最適化

改善されたのは Erlang のコード生成だけではない。JavaScript 側にも多くの進歩がある。

Gleam では、制御フローは case 式のパターンマッチで表現され、コンパイルするとネストした if 文が生成される。パターンマッチは宣言的なので、コンパイラは実行時のロジックを並べ替えたり最適化したりして、分割統治戦略で正しい分岐を素早く見つけられる。

John Downey がこの処理を改善し、生成されるコードがよりフラットになった。ネストした if 文は単一の条件にまとめられ、中間変数も減っている。たとえば、次の Gleam コードは:

pub fn go(x) {
  case x {
    Wibble(1, 2) -> 1
    _ -> 2
  }
}

以前は、この短い Gleam コードから次のような長い JavaScript が生成されていた 2:

export function go(x) {
  if (isWibble(x)) {
    let $ = x[0];
    if ($ === 1) {
      let $1 = x[1];
      if ($1 === 2) {
        return 1;
      } else {
        return 2;
      }
    } else {
      return 2;
    }
  } else {
    return 2;
  }
}

今はこうなる:

export function go(x) {
  if (isWibble(x) && x[0] === 1 && x[1] === 2) {
    return 1;
  } else {
    return 2;
  }
}

良い改善だと言えるだろう。意外なことに、圧縮と gzip を経たバンドルサイズはほとんど変わらない(gzip は本当に魔法だ)。それでも生成されるコードの分岐は減り、JavaScript エンジンが最適化しやすくなっている。

ありがとう、John!

リストリテラルの最適化

Gleam と JavaScript ではリストの構文は同じだが、Gleam の不変で永続的なリスト型は、JavaScript の可変で連続的な配列型とは別物だ。Gleam コードを JavaScript にコンパイルするときは、リストリテラルを実行時のデータ構造を構築する JavaScript コードに変換しなければならない。たとえば、次の Gleam コードは:

let numbers = [1, 2, 3]

次の JavaScript コードになる 2。JavaScript 配列を構築し、それを Gleam リストに変換する関数に渡している。

const numbers = arrayToList([1, 2, 3])

今回のリリースで、短いリストリテラルには別のコードが生成されるようになり、配列を経由せず直接構築する。

const numbers = prepend(1, prepend(2, prepend(3, empty)))

最新の JavaScript エンジンでは相応の性能向上が得られ、短いリストを多用するプロジェクト、たとえば Lustre を使うプロジェクトでは特に効果が大きい。長いリストでは改善が見られなかったため、引き続き配列からリストへの変換を使っている。

貢献してくれた Giacomo Cavalieri に感謝!

TypeScript API のオーバーロード

JavaScript にコンパイルするとき、Gleam コンパイラはプログラマが定義したデータ構造に対して、JavaScript から使える関数も生成する。さらに TypeScript の宣言ファイルも出力でき、同じプロジェクト内で TypeScript と Gleam を完全に統合できる。

カスタム型ごとに生成される関数の一つに、ある値が特定のバリアントに属するかを判定するものがある。たとえば、次の型があるとする:

pub type Box(value) {
  Full(value)
  Empty
}

生成される関数には、次のような TypeScript 宣言が付く:

export function Box$isFull(value: any): value is Full<unknown>;

目ざとい TypeScript プログラマなら問題に気づくだろう。値の型が Box<number> だとすでに分かっている場合、この関数はコンテナ型を Full に絞り込めるはずだが、number の型引数が unknown に一般化されてしまい、型情報が失われる。これでは使いにくい。

Giacomo Cavalieri が定義にオーバーロードを加え、可能な限り型が保たれるようにした。

export function Box$isFull<I>(value: Box$<I>): value is Full<I>;
export function Box$isFull(value: any): value is Full<unknown>;

ありがとう Giacomo!!

他のビルドツール向けの改善

Gleam ユーザーは通常、gleam 実行ファイルに同梱された公式のビルドツールを使うが、別の環境で Gleam コードをコンパイルして使いたいこともある。たとえば Elixir や Erlang のプログラマが、Gleam で書かれた依存パッケージを使いたい場合だ。実行時は非常にうまくいく。これら 3 つの BEAM 言語の間には優れたゼロコストの相互運用性があるからだ。だがそこに至るまでが容易とは限らない。Elixir と Erlang の主要なビルドツールには、Gleam のサポートが組み込まれていないからだ。

gleam 実行ファイルは、他のビルドツールが使えるようにコンパイラの機能を公開するコマンドをいくつか提供している。今回のリリースではこれらのコマンドに複数の改善が入り、Elixir の Mix と Erlang の rebar3 で Gleam をサポートすることが目標だ。

Erlang 仮想マシンは、すべてのパッケージがコンパイラのバイトコードに加えて .app リソースファイルを持つことを要求する。以前は Gleam をサポートするビルドツールが自前でこれらのファイルを用意する必要があったが、BEAM へのコンパイル時に gleam が生成するようになった。

compile-package コマンドに --no-dev フラグが追加され、コンパイラは src ディレクトリからのみコードを読み込み、dev_dependencies に指定されたパッケージをスキップするようになった。

export package-information と export package-interface コマンドは、これまでファイルにしか書き出せなかった情報を stdout にも出力できるようになった。あわせて、export javascript-prelude と export typescript-prelude コマンドはファイルに書き出せるようになった。

これらの変更を貢献してくれた Rodrigo Álvarez に感謝する。Elixir の Mix ビルドツールが Gleam をサポートする日もそう遠くないはずだ。

言語サーバーのラベル対応

Gleam には優れた言語サーバーが標準で付属しており、language server protocol に対応するあらゆるエディタで IDE 機能を利用できる。残る主要な機能は、フィールドと引数のラベルへの完全対応くらいだった。Alistair Smith がこれを修正し、定義へのジャンプ、参照の検索、ラベルのリネームに対応させた。Alistair、ありがとう。この時間節約機能を待ち望んでいた人は多いはずだ。

ブラウザ上で Gleam コードをフォーマット

コンパイラには WebAssembly ビルドがあり、言語ツアーとプレイグラウンドはこれを使ってウェブブラウザ上で Gleam をコンパイルしている。John Downey が format_source 関数を追加し、ブラウザ内で Gleam コードのフォーマッタを実行できるようにした。この機能はまもなくプレイグラウンドにも組み込む予定だ。John、ありがとう!

いつも通り、より良いエラーメッセージ

エラーメッセージをできる限り明確で役立つものにすることは、我々にとって非常に重要だ。順調なときに使いやすいだけでは不十分で、本当にプログラマを助けるか、逆に追い詰めるかが問われるのはエラーが起きたときだ。

ちょっとした構文ミスは厄介だ。どこがどう間違っているのか分からないとなおさらだ。

0xda157 は、コードに git のマージコンフリクトマーカーが残っている場合の専用エラーを追加した。また、User(..lucy, score: 10) のレコード更新構文で元のレコードを間違った位置に書いてしまった場合(User(score: 10, ..lucy) など)のエラーも追加している。さらに、Gleam に存在しない手続き型の演算子(+= や *= など)にもエラーを出すようにした。

n0kk23 は、パターンマッチで | を使った場合のエラーメッセージを分かりやすくした。Gleam では不正な構文だが、Java など他の言語では合法な書き方だ。

Giacomo Cavalieri は、定数式で許可されていない二項演算子に対するエラーメッセージを改善し、あわせてこうしたエラーが存在するときのコンパイラの耐性も高めた 3。

Andrey Kozhev は、同じパッケージ内のモジュールが別モジュールのプライベート型やプライベート値を使おうとした場合のエラーメッセージに文脈を補い、その型や値が存在はするもののプライベートであることを伝えるようにした。依存パッケージのモジュールについては、プログラマが管理していないコードの情報を漏らさないよう、こうしたヒントは出さない。

最後に、James Dolan は型チェッカーを改善し、無効な型エイリアス定義がそのエイリアスの使用箇所すべてに連鎖的なエラーを引き起こさないようにした。

Gleam のデバッグ体験をより良くしてくれた皆に感謝する。

その他の貢献

バグ修正や使い勝手の向上に尽力してくれた以下の皆にも感謝する:

0xda157、Amr Kadry、Andrey Kozhev、Giacomo Cavalieri、Hari Mohan、Ian Chamberlain、Jack Programs、John Downey、Lillian Rose、Mar Bloeiman、mmustafasenoglu、Naomi Roberts、Rodrigo Álvarez、Senthilnathan、Surya Rose、Vivid。

彼らが実装した多数の修正と改善の詳細は changelog を参照してほしい。

支援のお願い

Gleam はどの企業にも属しておらず、スポンサーの支援だけで成り立っている。その多くは月額 5and20 ドルを赞助しており、Gleam は私にとって唯一の収入源だ。

コアチームのメンバーに妥当な報酬を支払うという目標に向けて大きく前進したが、まだ道半ばだ。このプロジェクト、またはコアチームのメンバーへの支援を検討してほしい。

出典: Gleam← ホームへ戻る