
2026/05/10 2:52
Show HN: Go で作成した、Clojure に似た言語を公開します。起動までの時間はわずか 7 ミリ秒です。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Let-go は、Clojure に類似する言語のために設計されたバイトコードコンパイラおよび仮想マシンであり、同ファミリー内で最小で最も起動が速い選択肢を目指しています。コードを外部インフラストラクチャなしで動作するスタンドアロンのバイナリまたは WebAssembly アプリケーションに直接コンパイルします。主要なパフォーマンス指標には、約 10MB のバイナリサイズ、約 6-7ms のコールドスタート、低いアイドルメモリ使用量(約 14MB)が含まれ、これにより Babashka、GraalVM native、Joker、標準的な JVM 環境と比較して著しく小さく高速化しています。
このツールは、
core、core.async、HTTP、JSON などのほぼすべてのコア Clojure ライブラリ(マクロ、プロトコル、トランスデューサー、永続データ構造など)をサポートし、標準的な clojure-test-suite の 95.4% を通過する強力な互換性を提供します。core.async チャンネル、HTTP サーバー、JSON/Transit、IO、およびバイナリプロトコル経由の Babashka pod の読み込み(データベース、AWS、Docker など)を含む「ボックスセット」機能をサポートしています。高度な機能としては、Go との相互運用性があり、Go アプリケーションへの埋め込みをサポートし、機能マッピングと双方向の呼び出しを可能にします。
展開オプションは柔軟です:ユーザーは Homebrew または Go モジュールを使用して自己完結型のバイナリを作成したり、ターミナルエミュレーションを含むブラウザ実行のための WebAssembly にコンパイルしたり、Emacs、VS Code、Neovim などのリッチなエディタサポートのための nREPL サーバーを利用できます。非常に効率的ですが、標準的な Clojure/Java ランタイムに見られる特定の機能(Refs/STM は atoms+channels で置き換えられ、Spec、
deftype、読み込みタグ付きリテラル #inst など)は除外されています。
Text to translate:
Summary:
Let-go is a bytecode compiler and virtual machine for a language resembling Clojure, designed to be the smallest and fastest-starting option in the family. It compiles code directly into standalone binaries or WebAssembly applications that require no external infrastructure to run. Key performance metrics include a ~10MB binary with approximately 6-7ms cold starts and low idle memory usage (~14MB), making it significantly smaller and faster than alternatives like Babashka, GraalVM native, Joker, and standard JVM environments.
The tool offers robust compatibility by supporting nearly all core Clojure libraries (including
core, core.async, HTTP, JSON) and features like macros, protocols, transducers, and persistent data structures, passing 95.4% of the standard clojure-test-suite. It enables "batteries included" functionality with support for core.async channels, HTTP servers, JSON/Transit, IO, and Babashka pod loading (e.g., databases, AWS, Docker) over a binary protocol. Advanced features include Go interop, allowing embedding in Go apps with feature mapping and bidirectional calls.
Deployment options are flexible: users can create self-contained binaries via Homebrew or Go modules, compile to WebAssembly for browser execution with terminal emulation, and utilize an nREPL server for rich editor support (Emacs, VS Code, Neovim). While highly efficient, it excludes certain features found in standard Clojure/Java runtimes, such as Refs/STM (replaced by atoms+channels), Spec,
deftype, and reader tagged literals like #inst.本文
こんにちは、旅人の方!(λガファーはご存知ですか?笑)
これは、Clojure と極めて類似した言語(つまり Clojure の方言とでも呼ぼうか)のためのバイトコードコンパイラおよび仮想マシン(VM)です。Go で書かれた最も小さく、起動が最も速い Clojure 系言語です。約 10MB バイナリで、冷始動時間が約 7ms です。なぜ「let-go」なのでしょうか?
なぜ「let-go」なのか?
- 独立した実行ファイル —
コマンドでプログラムを単一の実行ファイルにコンパイルできます。ランタイムは不要です。 merely 配布してそのまま実行するだけです。lg -b myapp main.lg - WASM ウェブアプリケーション —
で、独立した HTML ページとしてプログラムをコンパイルできます。xterm.js を通じて完全なターミナルエミュレーションを行い、あらゆるブラウザで動作します。GitHub Pages へデプロイするか、ローカルで開いてください。lg -w outdir main.lg - 高速起動 — 冷始動が 6ms。プリコンパイルされたバイトコード(LGB フォーマット)のため、標準ライブラリが大きくても起動がほぼ瞬時です。
- 軽量なフットプリント — バイナリサイズは 10MB、アイドル時のメモリ使用量は 14MB です。Babashka より 7 倍、JDK より 30 倍小さいです。
- 機能の備えつき — core.async チャンネル、HTTP サーバー/クライアント、JSON、Transit、IO、Babashka pods、nREPL サーバーなど一应俱全です。
- Go との相互運用性 — let-go を Go アプリに埋め込み、Go の構造体(struct)をレコードにマッピングしたり、let-go から Go 関数を呼び出したり、その逆も行えます。
- 広範な Clojure 互換性 — マクロ、デストラクチャリング、プロトコル、レコード、マルチメソッド、トランスデューサー、遅延シーケンス、永続的なデータ構造、BigInt などに対応しています。
以下に挙げているのは漠然とした目標ですが、これらには優先順位はありません:
- 質の高い娯楽を提供すること、
- 本日の Go デイジョブにおいて Clojure を書くことを合法化すること、
- 永続的なデータ型、真の並行性、トランスデューサー、core.async、BigInt などを含め、Clojure の機能を可能な限り実装すること、
- 任意の関数およびタイプについて快適な双方向の相互運用性を提供すること、
- AOT コンパイル(let-go プログラムをバイトコードまたは独立したバイナリにコンパイル)すること、
- すべてのランタイムを単一の requestAnimationFrame で起動し、60fps で動作すると同時に 10ms の余裕を残すこと、
- ターミナルエミュレーション機能を備えた独立した WASM ウェブアプリケーションとして let-go プログラムをコンパイルすること、
- WASM 内の nREPL サーバー — ブラウザ内で動作する let-go VM に Emacs/Calva を WebSocket で接続すること、
- 延長目標:let-go バイトコードから Go への翻訳。
以下に挙げているのは「ないこと(非目標)」です:
- いつの時点においても
またはclojure
のドロップイン置換になることではないこと、clojure - Clojure 全般に対してリンター、フォーマッター、ツールの役割を担うことではないこと。
機能概観
let-go は Clojure をそのまま置き換えるものではなく、日常使いの Clojure に似た感触を意図しています。最もアイディオマティックなコードは読みやすく、動作し、振る舞いも同じである一方、大規模な非自明な Clojure プロジェクトでは、未改修で実行できるようにするにはいくつかの調整が必要になる可能性が高いです。詳しくは「既知の制限事項」を参照してください。
Clojure 互換性
let-go は、クロスダイアレクト準拠スイートの
jank-lang/clojure-test-suite に対してテストされています。
217 のテストファイル全体で 4696/4921 のアサーションが通り (95.4%) です。残りのギャップは主にエッジケース(+/-/*/inc/dec でのオーバーフロー検出、Long バウンダリでの BigInt プロモーション、BigDecimal の挙動)およびいくつかのスタブネームスペースに限定されています(詳しくは下を参照)。ワークフローガイド:docs/clojure-test-suite.md。
標準ネームスペース
| ネームスペース | ステータス | 詳細 |
|---|---|---|
| マクロ、デストラクチャリング、遅延シーケンス、トランスデューサー、プロトコル、レコード、マルチメソッド、アトム、正規表現、メタデータ、BigInt | |
| 完全対応 | |
| 完全対応 | |
| prewalk, postwalk, keywordize-keys, stringify-keys, walk | |
| read, read-string | |
| pprint, cl-format | |
| deftest, is, testing, are, fixtures | |
| チャンネル、go/go-loop、alts!, mult/pub、pipe/merge/split (Goroutine を使用、IOC は非対応) | |
| 多態的なリーダー/ライター、slurp/spit、遅延行シーケンス、エンコーディング、URL、with-open | |
| Ring スタイルのサーバー + クライアント、ストリーミングレスポンス | |
| read-json, write-json — 浮動小数点保存、レコード認識型 | |
| transit+json コーデック(ローリングキャッシュ付き) | |
| sh, stat, ls, cwd, getenv/setenv, exit, os-name, arch, user-name, hostname, separators | |
| JVM シック: getProperty, getProperties, getenv, exit, lineSeparator, currentTimeMillis, nanoTime。let-go.version, let-go.commit, user.home, user.dir, os.name, os.arch などを公開します。 | |
| システムプログラミング用の直接 Linux syscalls (mount, unshare, mknod, prctl, capset, seccomp, AppArmor) | |
| JSON / EDN / transit を介した Babashka pods |
Babashka Pods
let-go は Babashka pods - バイナリプロトコルを通じてネームスペースを公開する独立したプログラム - をサポートします。これにより、let-go はデータベース、AWS、Docker、ファイル監視など、ポッドエコシステム全体のアクセスが可能になります。
;; ポードをロード (babashka の共有キャッシュを使用) (pods/load-pod 'org.babashka/go-sqlite3 "0.3.13") ;; 他のネームスペースのように使用 (pod.babashka.go-sqlite3/execute! "app.db" ["create table users (id integer primary key, name text)"]) (pod.babashka.go-sqlite3/execute! "app.db" ["insert into users values (1, ?)" "Alice"]) (pod.babashka.go-sqlite3/query "app.db" ["select * from users"]) ;; => [{:id 1 :name "Alice"}]
- NAME (PATH) または babashka キャッシュ(シンボル + バージョン)からロードpods/load-pod- JSON、EDN、transit+json ペイロード形式をサポート
- クライアント側コード評価(ポード定義されたマクロとラッパー)
を通じた非同期ストリーミング(ポード/invoked でのコールバック):handlers- ~/.babashka/pods/ キャッシュを共有 — bb でポードをインストールし、lg から使用
利用可能なポードについてはポードレジストリをご覧ください。bb を使用してポードをインストールします:
bb -e '(pods/load-pod (quote org.babashka/go-sqlite3) "0.3.13")'
Go 相互運用性
— キャッシュされたフィールドコンバーターを持つ Go 構造体を let-go レコードにマッピングRegisterStruct[T]
— 変更のないレコードのゼロコストラウンドトリップToRecord[T] / ToStruct[T]
— 登録された構造体を自動でレコードに変換BoxValue
— Go の値はBoxed
相互運用構文を通じてメソッドを公開.method- レコード上の
アクセス.field
ベンチマーク
ベンチマークでは、let-go を Babashka (GraalVM ネイティブ)、Joker (Go ツリートループインタプリタ)、および JVM 上の Clojure と比較しています。各ベンチマークはすべてのランタイムで未改修に動作する有効な Clojure コードです。再現するには
benchmark/run.sh を実行してください(hyperfine, bb, clj, joker が必要)。
| プラットフォーム | Go バイトコード VM (let-go) | GraalVM ネイティブ (babashka) | Go ツリートループインタプリタ (joker) | JVM (HotSpot) |
|---|---|---|---|---|
| バイナリサイズ | 10M | 68M | 26M | 304M (JDK) |
| 起動時間 | 7ms | 20ms | 12ms | 331ms |
| イーデントメモリ | 14MB | 27MB | 21MB | 92MB |
パフォーマンスのハイライト (Apple M1 Pro):
- 最も小さいフットプリント — Babashka より 7 倍、JDK より 30 倍小さい
- 最速の起動 — プリコンパイルされたバイトコードで 7ms (requestAnimationFrame に収まる)、Babashka より 3 倍、Joker より 2 倍、JVM より 48 倍速い
- 短いライフサイクルタスクでの優位 — map/filter およびトランスデューサーパイプライン:8ms vs bb の 19ms (2.4 倍速い)
- 計算能力における競争力 — fib(35) で Babashka にほぼ匹敵(1.98s vs 1.90s)、loop-recur は 1.8 倍速い
- 最低のメモリ使用量 — fib(35) で 14MB vs bb の 77MB (5.4 倍少ない)、reduce 1M で 20MB vs bb の 59MB (3 倍少ない)
- 大多数の計算ベンチマークで Joker より 10 倍以上速い — バイトコード VM vs ツリートループインタプリタ
詳細な結果と方法論:
benchmark/results.md
Clojure との既知の制限事項および乖離点
実装されていないもの
- Refs / STM — アトム + チャンネルが実用的な並行性のニーズをカバー
- Agents — go ブロックとチャンネルを使用してください
- 階層 (derive, underive, ancestors, descendants, parents) — スタブのみ;マルチメソッドディスパッチは動作しますが、isa? チェインは動作しません
- with-precision — BigDecimal 自体は動作します(M リテラル、bigdec、decimal?、正確な算数)、しかし with-precision はノオプのため、明示的な丸め制御が欠けています
- チャンク化されたシーケンス — 遅延シーケンスはチャンク化されていません(シンプルで、少し異なるパフォーマンス特性)
- リーダータグ付きリテラル (#inst, #uuid)
- deftype — defrecord を使用してください
- reify — プロトコルは命名されたタイプにのみ拡張可能
- Spec — clojure.spec はありません
- alter-var-root — 変数は変更可能ですが、alter-var-root はありません
- 数値オーバーフロー検出 — +/-//inc/dec は int64 オーバーフロー時に BigInt にプロモートせず、静かにラップします。明示的な BigInt 算数には +'/-'/' を使用してください
- subseq / rsubseq — ソートされたコレクション自体は動作しますが (sorted-map, sorted-set, sorted-map-by, sorted-set-by, rseq)、それらへの範囲クエリはまだ実装されていません
既知の挙動の違い
(quasiquote で内部で使用) は eager です。ユーザー向け concat は遅延しており、Clojure に適合していますconcat*- すべてのチャンネル操作はブロックします —
と<!
は同一です(Go チャンネルは常にブロッキング)、<!!
も同様です>!/>!! - go ブロックは真の Goroutine です — Clojure の core.async 様の IOC (制御の反転) 状態機械はありません;これによりコストが低くなりますが、go ブロックは直接ブロッキング操作を呼び出せる
- BigDecimal はありません — 数値タワーは int64 + float64 + BigInt(任意精度小数なし)です
- Regex は Go フレーバーです — re2 シンタックスで、Java レギュラー表現ではありません
は内部でアトムを使用して前方参照を行います — Clojure の直接的なバインド vs 若干のオーバーヘッドletfn
例
let-go で書かれた実際のプロジェクト:
- xsofy — 同じソースコードからブラウザとターミナルで動作するログライクゲーム
- lgcr — syscall ネームスペースを基盤とした優れた daemonless コンテナランタイム
このリポジトリ内では:
— 小規模なプログラムexamples/
— すべての機能をカバーする .lg テストファイルtest/
オンラインで試す
最小限のオンライン REPL をチェックアウトしてください。ブラウザ内で let-go の WASM ビルドを実行しています!
インストール
Homebrew (macOS / Linux)
brew tap nooga/let-go https://github.com/nooga/let-go brew install let-go
バイナリをダウンロード
Releases からプリビルトバイナリを取得してください。Linux、macOS、Windows の amd64/arm64 で利用可能です。
ソースコードから
Go 1.22+ が必要です。
go install github.com/nooga/let-go@latest
使用方法
lg # REPL lg -e '(+ 1 1)' # 式を評価 lg myfile.lg # ファイルを実行 lg -r myfile.lg # ファイルを実行し、その後 REPL を開始 lg -w outdir myfile.lg # WASM ウェブアプリケーションにコンパイル
コンパイルと配布
let-go はプログラムをバイトコード (.lgb ファイル) にコンパイルし、独立した実行ファイルとしてパッケージ化できます。
バイトコードへコンパイル — ロード時により Reader/パーサー/コンパイラをスキップ:
lg -c app.lgb app.lg # バイトコードへコンパイル lg app.lgb # バイトコードを直接実行
独立したバイナリを作成 — コンパイルされたバイトコードを自己完結型実行ファイルにバンドル:
lg -b myapp app.lg # コンパイル + バンドリングで実行可能 ./myapp # 任意の場所で動作、lg が不要
独立したバイナリは lg のコピーで、あなたのプログラムのバイトコードが追加されています。外部ファイルやランタイムを必要としません。他のマシンにコピーしてそのまま実行するだけです。
WASM ウェブアプリケーションを作成 — プログラムをブラウザで動作する単一 HTML ページにコンパイル:
lg -w site app.lg # ウェブアプリケーションへコンパイル open site/index.html # ブラウザで開く
出力ディレクトリには以下が含まれます:
— 自己完結型 (~6MB、インラインされた WASM + wasm_exec.js、gzip 圧縮)index.html
— インタラクティブなアプリケーションへのクロスオリジンアイソレーションを有効化 (GitHub Pages で必要)coi-serviceworker.js
term ネームスペースを使用するプログラムは xterm.js を通じて完全なターミナルエミュレーションを受け取ります。ANSI カラー、カーサー位置、生キヤードボード入力がすべて動作します。Go WASM ランタイムは SharedArrayBuffer を備えた Web Worker で動作し、ブロッキング term/read-key を処理します。GitHub Pages デプロイメントには、出力ディレクトリをポインタに指すだけで十分です。サービスワーカーが自動的に必要な COOP/COEP ヘッダーを処理します。
AOT コンパイルの検出 — *compiling-aot*
変数は -c, -b, -w コンパイル中に true、実行時には false です:
*compiling-aot*(defn -main [] (start-server)) (when-not *compiling-aot* (-main))
WASM での実行時の検出 — *in-wasm*
変数は WASM ウェブアプリケーション内で動作中なら true、ネイティブモードでは false です:
*in-wasm*(when-not *in-wasm* (spit "debug.log" "only in native mode"))
ソースコードからのビルド
go run . # ソースから実行 go build -ldflags="-s -w" -o lg . # ~9MB ストリップ済みバイナリ
nREPL
let-go は CIDER (Emacs)、Calva (VS Code)、Conjure (Neovim) と互換性の nREPL サーバーを内蔵しています。
— デフォルトポート (2137) で nREPL を開始lg -n
— ポート 7888 で nREPL を開始lg -n -p 7888
サーバーは現在のディレクトリに
.nrepl-port を書き込み、エディタが自動で発見できるようにします。
サポートされる操作:clone, close, eval, load-file, describe, completions, complete, info, lookup, ls-sessions, interrupt
- Emacs (CIDER): M-x cider-connect-clj、ホスト localhost、ポートは .nrepl-port から
- VS Code (Calva): let-go プロジェクトを開く — 内蔵された
がカスタムコネクトシーケンスを登録します。「Calva: Start a Project REPL and Connect (Jack-In)」を選択し、「let-go」を選択するか、nREPL が既に動作している場合は「Calva: Connect to a Running REPL Server」を使用してください。.vscode/settings.json - Neovim (Conjure):
が存在すると自動的に接続されるはずです.nrepl-port
Go への埋め込み
let-go は Go プログラムのスクリプティングレイヤーとして清潔に埋め込まれます — Go の値と関数を定義し、VM に渡して、ユーザーが提供した Clojure をあなたのデータに対して実行します。Go 構造体はレコードとしてラウンドトリップし、Go チャンネルは一級 let-go チャンネルとなり、Go 関数は let-go コードから呼び出せます。
import ( "github.com/nooga/let-go/pkg/api" "github.com/nooga/let-go/pkg/vm" ) c, _ := api.NewLetGo("myapp") // Go の値と関数を let-go に公開 c.Def("x", 42) c.Def("greet", func(name string) string { return "Hello, " + name }) v, _ := c.Run(`(greet "world")`) fmt.Println(v) // "Hello, world"
- 構造体 ↔ レコードのラウンドトリップ: 登録された構造体は let-go 側でレコードになります。変更のない値は元々の Go タイプにゼロコストでアンボックスされます;変更のある値は
を通じます。vm.ToStruct[T]type Item struct{ Name string; Price float64; Qty int } vm.RegisterStruct[Item]("myapp/Item") c.Def("item", Item{Name: "Widget", Price: 9.99, Qty: 5}) c.Run(`(:name item)`) // "Widget" c.Run(`(* (:price item) (:qty item))`) // 49.95 // Go 構造体を処理する let-go 関数を定義 c.Run(`(defn total [it] (* (:price it) (:qty it)))`) v, _ := c.Run(`(total item)`) // 49.95 - Go チャンネルによるストリーミング: Go chan int と vm.Chan は
に直接プラグインします — ユーザーが提供したスクリプトを通じてイベントをパイプするのに最適です。go/<!/>!inch := make(chan int) outch := make(vm.Chan) c.Def("in", inch) c.Def("out", outch) c.Run(`(go (loop [i (<! in)] (when i (>! out (inc i)) (recur (<! in)))))`)
埋め込みの例(defs、構造体、チャンネル、関数呼び出し)の全セットについては
pkg/api/interop_test.go を参照してください。
テスト
go test ./... -count=1 -timeout 30s
純粋な Go の 20MB タイプチェックできる JS ランタイムで TypeScript を実行したいですか?他のプロジェクト https://github.com/nooga/paserati をチェックしてみてください。
🤓 X で私をフォローしてください 🐬 monk.io もご覧ください