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

Show HN: RustだけどLisp

概要

rlisp は、 LISP風S式Rustコード を書くための週末プロジェクト。 Rustの型・所有権システム を維持しつつ、 LISPのマクロ や構文を利用可能。 rlisp は構文変換のみ担当し、 rustc が型・所有権・最適化を担当。 Ariadne によるエラーダイアグノスティクス、 インストール・利用方法 も簡単。 SYNTAX.md で全機能を網羅、 MITライセンス で公開。

rlisp:RustセマンティクスをLISP構文で

  • rlisp はLISPの S式 でRustプログラムを記述し、Rustソースコードへ変換するツール
    • 例:(struct Point (x f64) (y f64)) → struct Point { x: f64, y: f64 }
  • 所有権・借用・ライフタイム・ジェネリクス・トレイト・パターンマッチ など、Rustの主要機能をS式で表現
  • rustc による型チェック・借用チェック・最適化を活用、 rlisp は構文変換に特化
  • Ariadne による分かりやすいエラー出力

インストール・使い方

  • インストール手順
    • git clone https://github.com/ThatXliner/rlisp.git
    • cd rlisp
    • cargo install --path .
  • コマンド例
    • rlisp compile file.lisp # Rustソース生成
    • rlisp build file.lisp  # Rustソース生成&コンパイル
    • rlisp run file.lisp   # 生成・コンパイル・実行

LISP→Rust 構文変換クイックリファレンス

  • 関数定義:(fn add ((x i32) (y i32)) i32 (+ x y)) ⇒ fn add(x: i32, y: i32) -> i32 { x + y }
  • 変数定義:(let x i32 42) ⇒ let x: i32 = 42;
  • 構造体:(struct Point (x f64) (y f64)) ⇒ struct Point { x: f64, y: f64 }
  • 列挙型:(enum Option (generic T) (Some T) None) ⇒ enum Option<T> { Some(T), None }
  • パターンマッチ:(match val ((Some x) (handle x)) (None ())) ⇒ match val { Some(x) => handle(x), None => {} }
  • if文:(if (> x 0) (println! "yes") (println! "no")) ⇒ if x > 0 { println!("yes") } else { println!("no") }
  • 実装ブロック:(impl Point (fn new (...) ...)) ⇒ impl Point { fn new(...) ... }
  • トレイト:(trait Display (fn fmt (...) Result)) ⇒ trait Display { fn fmt(...) -> Result; }
  • フィールド・メソッドアクセス:(. obj field) / (. obj method arg) ⇒ obj.field / obj.method(arg)
  • 構造体初期化:(new Point (x 1.0) (y 2.0)) ⇒ Point { x: 1.0, y: 2.0 }
  • ループ:(loop (body)) / (while cond (body)) / (for x in iter (body)) ⇒ loop { body } / while cond { body } / for x in iter { body }
  • クロージャ:(lambda (x y) (+ x y)) ⇒ |x, y| { x + y }
  • マクロ・プリント:(foo! args) / (println! "{}" x) ⇒ foo!(args) / println!("{}", x)
  • 公開関数:(pub fn foo () i32 42) ⇒ pub fn foo() -> i32 { 42 }
  • インラインRust:(rust "let x: i32 = 42; x") ⇒ let x: i32 = 42; x

詳細仕様と注意点

  • SYNTAX.md にて、 ジェネリクス・ライフタイム・可視性・モジュール・turbofish・if-let・unsafe など全構文を解説
  • 二項演算子 は自動で中置記法に変換:(+ a b) → (a + b)
  • ケバブケース識別子 はRust用に自動変換:page-header → page__header
    • 名前衝突時はコンパイル警告

マクロ機能

  • rlispマクロ は、 S式→S式 のコンパイル時変換関数
    • Rustの proc_macrosyn/quote 不要
  • マクロ定義はLISP同様、 quasiquote/unquote/unquote-splicing を使用
    • (quasiquote template):テンプレート全体をクオート、unquote箇所のみ展開
    • (unquote name):テンプレート内の穴埋め
    • (unquote-splicing name):リストを展開して挿入
  • 可変長引数 は&restで受け取り、unquote-splicingで展開
  • 例:(defmacro when (condition &rest body) (quasiquote (if (unquote condition) (do (unquote-splicing body)))))
    • (when (> x 10) (print "big") (print "huge")) ⇒ if x > 10 { print("big"); print("huge") }

ループ・クロージャ・モジュール

  • ループ例
    • (while (> x 0) (println! "{}" x) (-= x 1))
    • (loop (println! "tick"))
    • (for x in 0..10 (println! "{}" x))
  • クロージャ例
    • 型なし:(let add (lambda (x y) (+ x y)))
    • 型あり:(let mul (lambda ((x i32) (y i32)) i32 (* x y)))
    • move:(let s "hello") (let greet (lambda move () (println! "{}" s)))
  • モジュール・可視性・import
    • (pub fn public_api () i32 42)
    • (pub (crate) fn internal () i32 0)
    • (pub struct Config (pub host String) (port u16))
    • (mod external_lib)
    • (use std::collections::HashMap)

インラインRust・高度な構文

  • rlispで表現困難な場合、(rust "...")で生のRustコード挿入可能
  • ライフタイム・turbofish・制御フローもS式で表現
    • (fn longest (generic 'a) ((x &'a str) (y &'a str)) (&'a str) ...)
    • (let nums ((:: (. (0..10) collect) Vec<i32>)))
    • (if-let (Some v) (. map get key) (println! "found: {}" v) (println! "missing"))
    • (unsafe (rust "let ptr: *const i32 = &42;") (rust "*ptr"))

rlispの目的とメリット

  • 目的 :Rustの型・所有権システムを維持しつつ、LISPの 柔軟なマクロ均一なS式構文 を楽しむ
  • LISPマクロ の強み:proc_macro不要、S式を返すだけで強力な構文拡張が可能
  • 構造的編集 :S式は括弧が常に対応し、構造化編集が容易
  • 統一構文 :関数シグネチャもmatchアームも同じS式で記述、理解負荷が低減

ライセンス

  • MITライセンス で公開、誰でも自由に利用・改変可能

Hackerたちの意見

これって、Rustをs式の構文で書く感じで、ちゃんとしたLispの方言がRustにコンパイルされるわけじゃないみたいだね。クールではあるけど、あんまり面白くはないかな。Lispプログラミングをちょっとでもやったことがある人には、かなり変に見えると思う。

表現のスコープを超えるライフタイムを持つ変数を定義するlet?うん、それは本当に珍しいね。しかも、最初のコード例から見ても、これが一番変なものってわけでもないし。

Rustの意味論をLISP構文で。ランタイムもGCもなしで、Rustに直接コンパイルする透明なs式フロントエンド。最初の段落にそのまま書いてあるよ。

そうだね、いくつかのLispマシンのマイクロコードアセンブリを思い出すよ。s式ではあったけど、明らかにLispそのものではなかった。でも、Lispマクロの面白いターゲットになるかもしれないね。

いくつかのコメントは、まさにRustであることの利点を見逃してる気がする。新しい意味論がないなら、機械語にコンパイルできるLispならCommon Lispがかなり効率的になるよ。Rustを持ち込む目的は、Rust特有の意味論を引き出すことだから、多くの人がそれを結構好きなんだよね!

ってことは、Fennelみたいな感じなのかな?FennelはLuaのランタイムを完全には隠さないって意識してるし。https://fennel-lang.org/

すべての構文がカバーされてるって言ってるけど、ライフタイムやターボフィッシュを指定する例が一つもないのは、ちょっと残念だね。これらはRustの中でも特に難しい構文の一部なんだから。

なんか、バイブコーディングされたパーサーだね…

Rustでオプション指定の型(例えば、変数宣言)を表現する能力があるなら、ライフタイムやターボフィッシュも表現できるはずだよ。ターボフィッシュは特定の型パラメータを持つジェネリック関数を呼ぶためのちょっと変わった呼び方だけど。唯一変なところは、LispがアポストロフィをRustとは全然違う用途で使うことだけど、ライフタイムを示すために他の方法を選ぶこともできるよ。

HRTBはライフタイムを指定するのに一番難しい構文かも。こんな感じに見えるよ:for F: Fn(&'a (u8, u16)) -> &'a u8

もし何か実装し忘れたら、(rust "...")マクロを使って直接Rustに入れるよ。

読者のみんな、僕のリスプ「Loon」を楽しんでくれるかも。これはRustからの影響をたっぷり受けてるよ。 https://loonlang.com/guide/ownership

同意!所有権の概念を中心にしたリスプは、S式でRustを書く方法よりずっと面白いよね。

リスプの木構造の中で所有権はどう機能するの?この設定での所有権とARCの違いは何?

あのページめっちゃきれい!どんなSSGやテーマを使って作ってるの?

正直、めっちゃクールだね!このプロジェクトの意図はまさにそれだったんだけど、最も手抜きな方法でたどり着いたよ(笑)

どこにでもある型推論が好き。ちょっとEmacs LispのELSAを思い出すな: https://github.com/emacs-elsa/Elsa。特に、型を意識したマクロはずっと欲しかった。例えば、推論された型に基づいて操作を特化させるelispやCL/SBCLのコンパイラマクロが書けない理由はないと思う。普通のLispでは、宣言された型すら取得するのが難しいからね。とはいえ、Loonのその部分がアロケーションモデルにあまり依存していないといいな。なんで高レベルな言語で手動のメモリ管理を必須にしたの?それとエフェクトについても。言語設計でよく見かける2つのことが、正直言って不要に思えるんだ:1. 手動アロケーションとライフタイムスタッキング、2. 代数的エフェクト。1について:Rustスタイルの可変性とエイリアス参照の利点を、literal mallocとfreeを使う利点と混同していることが多いと思う。前者は後者を必要とせずに実現できるし、そうすることでより良い言語体験が得られると思う。GCが「遅延スパイクや高いメモリ使用量、不確定なポーズを伴う」というのは、現代の実装ではあまり意味のあることじゃないよ。むしろ、より一貫したレイテンシ(予測できないタイミングでの巨大な木の同期的Dropがない)と良いメモリ使用(良いGCは圧縮ポインタや圧縮を使うから)をもたらすと思う。2について:限定された継続のための非代数的エフェクトは理解できる。でも最近、非フローマジカルなエフェクトを何にでも使っている人を見かける。データベースとやり取りする必要があるなら、データベースインターフェースを選んで、そのインターフェースを実装したオブジェクトを必要なコードに渡せばいい。エフェクトも基本的には同じことをするけど、暗黙的にね。

好きだけど、似たようなloonlang.orgがあって混乱するな。

もしLispと所有権が好きなら、Carpもあるよ [1]。ただ、Rustの機能や命名規則を真似てるわけじゃないけど。Carpは約10年前にできて、クールなデモもあるよ(ゲーム開発用のSDLみたいな)。 > Carpの主な特徴は以下の通り: > * 自動かつ決定論的なメモリ管理(ガベージコレクタやVMなし) > * 高速で信頼性の高い静的型推論 > * 所有権追跡により、キャッシュフレンドリーなデータ構造の変更を使いつつ関数型プログラミングスタイルを実現 > * 隠れたパフォーマンスペナルティなし – アロケーションとコピーは明示的 > * 既存のCコードとの簡単な統合 > * Lispマクロ、コンパイル時スクリプト、便利なREPL [1]: https://github.com/carp-lang/Carp

もしこれを実際に使いたいと思って、Rust風のリスプコードを書いたとして、コンパイルエラーが出たら、エラーが起きた場所を指してくれる素敵なエラーメッセージが表示されるのかな?それとも、すごいrust-analyzer LSPを使ってクールなIDE機能が使える?答えは「いいえ」だと思うけど、これはいいさらなるプロンプトになるかも。

いいアイデアだね!今、エラーメッセージとスパンをアリアドネ風に追加してるところだよ。

かなり面白いね。今のところ、コードはコンパイルできないみたい。codegen.rsに「span」関連の余計なものがあって、main.rsではWarningDisplayを実装してないからフォーマットしようとしてる。これを直すと、ほぼ広告通りに動くけど、一文字の型が常にジェネリックパラメータだと仮定しているみたいで、例えばこれを生成するのは不可能だね: struct X; enum A { P(X), Q } これを試すと:(struct X) (enum A (P X) Q) こうなる: struct X; enum A { Q } でも、Stringのような複数文字の型を使うと:(enum A (P String) Q) 期待通りになる: enum A { P(String), Q } これを解決する一つの方法は、常にジェネリックアノテーションを必要とすることで、ジェネリックがないときは空にすればいいんだけど、それを試したら変なことになった:(struct X) (enum A () (P X) Q) こうなる: struct X; enum A { _ /* List([], Some(Span { start: 54, end: 56 })) */, P(X), Q } _とコメントがどこから来たのか全くわからない。

((. dx powf 2.0) + (. dy powf 2.0)) sqrt)) これが何かはわからないけど、明らかにLispじゃないね…

これ、Cでもやってるの見たことあるよ。 https://www.eriksvedang.com/carp https://github.com/tomhrr/dale

CarpはRustのように線形型と参照を使ってメモリ安全だから、Cっぽいとは言えないけど、むしろRustっぽいね。

Rustの構文についての記事のコメントで、Rustの代わりにS式ベースの構文を考えたときのことを思い出すなぁ。https://news.ycombinator.com/item?id=41399712

実際、普通のRustよりこっちの方が好きかも…