let-go: Goのための高性能なClojure方言

Clojureの表現力豊かなパワーを愛しながらも、Goのデプロイの簡便さを必要とする開発者にとって、let-goは魅力的な中間地点を提供します。これはGoで実装されたClojure方言のバイトコードコンパイラおよびVMであり、速度、小さなフットプリント、および配布の容易さに特化して設計されています。

軽量なCLIツール、WASMベースのWebアプリケーション、あるいは既存のGoアプリケーションのスクリプティングレイヤーを構築したい場合、let-goは、約7msで起動するランタイムを提供します。これにより、JVMやGraalVM-nativeバイナリに代わる魅力的な選択肢となります。

コアアーキテクチャとパフォーマンス

let-goの本質は、バイトコードVMです。Clojure風のソースコードを事前コンパイルされたバイトコード形式(.lgb)にコンパイルすることで、ランタイムはロード時にリーダー、パーサー、およびコンパイラフェーズをスキップし、即座に近い起動を実現します。

他のランタイムとのベンチマーク

パフォーマンスの向上は、他のClojure実装と比較したときに最も顕著になります。Apple M1 Proにおいて、let-goはフットプリントと起動速度において大きな利点を示しています:

メトリック let-go Babashka Joker Clojure JVM
バイナリサイズ 10MB 68MB 26MB 304MB (JDK)
起動時間 7ms 20ms 12ms 331ms
アイドルメモリ 14MB 27MB 21MB 92MB

起動以外にも、let-goは計算負荷の高いタスクにおいて非常に競争力があります。例えば、フィボナッチ数列の計算においてBabashkaの4%以内の誤差で動作し、ツリーウォーク・インタープリタを使用するJokerよりも約10倍高速です。

主要な機能と能力

スタンドアロン実行ファイルとWASM

let-goの主な目標の一つは、ランタイムの必要性を排除することです。ユーザーはlg -bを使用してプログラムを単一のスタンドアロンバイナリにコンパイルでき、これによりバイトコードがlg実行ファイルに同梱されます。

さらに、let-goはWASMへのコンパイルをサポートしています。lg -wを使用すると、プログラムは自己完結型のHTMLページにコンパイルされます。これにはxterm.jsを介した完全なターミナルエミュレーションが含まれており、Clojure風のプログラムをブラウザでネイティブな感覚のターミナル体験とともに直接実行することを可能にします。

Goとの相互運用性

let-goは組み込み(embedded)されるように設計されています。Goの値をおよび関数をVMに公開することで、Go開発者がこの言語をスクリプティングレイヤーとして使用することを可能にします。主な相互運用機能は以下の通りです:

  • Struct Mapping: Goの構造体はlet-goのレコードに登録およびマッピングでき、不変データに対してゼロコストのラウンドトリップを実現します。
  • Channel Integration: Goのチャンネル(chan)はlet-gocore.async実装に直接プラグインでき、2つの言語間のシームレスなイベントパイピングを可能にします。
  • Function Calls: Goの関数はlet-goのコードから直接呼び出せ、その逆も可能です。

エコシステムとの互換性

Clojureとの高い互換性を維持するために、let-goclojure-test-suiteに対してテストされており、95%以上の断言(assertions)をパスしています。clojure.coreclojure.stringclojure.set、およびcore.asyncといった不可欠な名前空間を実装しています。

興味深いことに、let-goBabashka podsもサポートしています。これにより、バイナリプロトコルを介して名前空間をexposeするスタンドアロンプログラムの広大なエコシステムにアクセスでき、ライブラリエコシステムを書き直すことなく、データベース、AWS、およびDocker統合に即座にアクセスできます。

標準的なClojureとの違い

let-goは高忠実度な体験を目指していますが、JVM Clojureのドロップイン・リプレイスメント(そのまま置き換えられるもの)ではありません。開発者はいくつかの重要な違いに留意する必要があります:

  • Concurrency: RefsやSTM (Software Transactional Memory) は欠如していますが、atomsやchannelsは完全にサポートされています。
  • Numeric Behavior: int64における数値のオーバーフローは、自動的にBigIntに昇格するのではなく、サイレントにラップされます。ユーザーは明示的なBigInt数学関数(例:+'の代わりに+)を使用する必要があります。
  • Async Implementation: JVM版とは異なり、let-goにおけるgoブロックは実際のgoroutineであり、IOC状態マシンではありません。これにより、コストは低くなりますが、直接ブロッキング操作を呼び出すことが可能になります。
  • Regex: Javaの代わりに、Goのre2形式の正規表現を使用します。

はじめに

実験に興味がある方は、let-goはHomebrewまたはGo経由でインストールできます:

brew tap nooga/let-go
brew install let-go

またはGo install経由:

go install github.com/nooga/let-go@latest

インストール後、lgでREPLを開始するか、lg myfile.lgでファイルを走らせることができます。高度なワークフローの場合、lg -nコマンドはCIDER、Calva、およびConjureと互換性のあるnREPLサーバーを開始し、Clojureの強力な対話型開発体験をGoのエコシステムに持ち込みます。

Sources