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

Rustに触発された小さな言語「Lisette」がGoにコンパイルされる

2026年4月5日原文(lisette.run)

概要

  • Lisette はRustに着想を得た 小型言語 で、Goに コンパイル される
  • 代数的データ型パターンマッチHindley-Milner型推論デフォルト不変 などをサポート
  • Goエコシステムとの 高い互換性 を持つ
  • nil禁止エラー安全型安全なAPI設計 が特徴
  • VSCode/Neovim/Zed などの LSP対応 も完備

Lisette言語の特徴

  • Rust風構文Go互換性 の両立

    • Goのパッケージをimport "go:..."で直接利用可能
    • cargo install lisetteでインストール
  • 代数的データ型(ADTs)とパターンマッチ

    • enumで複数バリアントを定義
    • match式で安全に分岐
    • 網羅性チェック による安全性強化
  • 構造体とimplブロック

    • structでデータ構造定義
    • implでメソッド実装
    • 不変デフォルト明示的可変(let mut)
  • 式指向・if let・let else構文

    • すべてが式として値を返す
    • if letlet elseによるパターンバインディング
  • インターフェースとジェネリクス

    • interfaceで抽象化
    • 型パラメータによる汎用関数
  • Option/Resultによるエラー・欠損値管理

    • nil禁止、欠損はOption<T>、エラーはResult<T, E>
    • ?演算子でエラー伝播
    • Resultの未処理検知 ・明示的破棄サポート
  • Goとのシームレスな型変換

    • Lisetteコード→Goコードへの安全な変換
    • 例:パターンマッチやエラー伝播がGoのif文・switch文に自動変換
  • パイプライン演算子・ラムダ式・メソッドチェーン

    • |>で関数合成
    • 無名関数・チェーン処理で記述性向上
  • 非同期・並行処理対応

    • taskでGoのgoroutine相当
    • Channel型とselect式で競合解決
  • tryブロック・panic/recover

    • 例外的状況も型安全に処理
    • recoverでpanicから安全に復帰
  • 属性によるシリアライズ・バリデーション

    • #[json(...)]#[tag(...)]で詳細な制御
  • 詳細なエラーメッセージとヒント

    • 網羅性・可変性・型の公開範囲・未初期化などを静的検査
    • 例:matchの未カバー・nil禁止・private type公開警告

Lisette言語の主な構文例

  • パターンマッチ

    enum Message { Ready, Write(string), Move { x: int, y: int } }
    fn handle(msg: Message) -> string {
      match msg {
        Message.Ready => "ready",
        Message.Write(text) => f"wrote: {text}",
        Message.Move { x, y } => f"move to ({x}, {y})",
      }
    }
    
  • if let/let else

    type Headers = Map<string, string>
    fn handle_headers(h: Headers) -> Result<(), string> {
      if let Some(token) = h.get("Authorization") {
        let user = authenticate(token)?
        authorize(user)?
      } else {
        return Err("missing credentials")
      }
      let Some(id) = h.get("X-Request-ID") else {
        return Err("missing request ID")
      }
      process(id)
    }
    
  • パイプライン演算子

    import "go:strings"
    fn slugify(input: string) -> string {
      input |> strings.TrimSpace |> strings.ToLower |> strings.ReplaceAll(" ", "-")
    }
    slugify(" Hello World ") // => "hello-world"
    
  • 並行処理

    let ch1 = Channel.new<string>()
    let ch2 = Channel.new<string>()
    task { ch1.send(fetch_primary()) }
    task { ch2.send(fetch_replica()) }
    let quickest = select {
      match ch1.receive() { Some(v) => v, None => "closed", },
      match ch2.receive() { Some(v) => v, None => "closed", },
    }
    
  • Goへのコンパイル例

    // Lisette
    fn classify(opt: Option<int>) -> string {
      match opt {
        Some(x) if x > 0 => "positive",
        Some(x) if x < 0 => "negative",
        Some(_) => "zero",
        None => "none",
      }
    }
    // Go
    func classify(opt lisette.Option[int]) string {
      if opt.Tag == lisette.OptionSome {
        x := opt.SomeVal
        if x > 0 { return "positive" }
        if x < 0 { return "negative" }
        return "zero"
      }
      return "none"
    }
    

静的検査・エラーメッセージ例

  • match網羅性チェック
    • 未カバー分岐を検出し、ヒントを表示
  • nil禁止
    • 欠損値は必ずOption<T>で表現
  • Result未処理警告
    • ?matchで必ずハンドリング
  • private type公開警告
    • 公開APIにprivate型を含めると警告
  • 不変変数の可変渡し警告
    • let mutで明示的に可変化
  • 構造体の未初期化警告
    • すべてのフィールド初期化が必須

開発環境サポート

  • LSP対応 :VSCode、Neovim、Zedなど主要エディタ
  • Goエコシステムとの連携 :Goのライブラリ・ツールチェーンを活用可能

Lisetteは 型安全性・エラー安全性・Go互換性 を高次元で両立した、 現代的なシステムプログラミング言語。Rustの洗練とGoの実用性を両取りしたい開発者に最適。

Hackerたちの意見

これ、すごくいい仕事だね。エラーメッセージだけでも、かなり気を使ってるのがわかるし、「ヘルプ」ヒントも本当に役に立ちそう。コンパイラのノイズじゃない感じがする。だけど、コンパイルされたGoの出力が気になるな。Resultのデスugarが結構冗長になるけど、生成されたコードには全然問題ないよね。でも、ランタイムで何かが壊れた時は、たぶんGoを読んでるんじゃなくてLisetteを読んでることになるよね。LSPはエラーをソースの位置にマッピングするのを扱ってるの?それと、既存のGoコードからLisetteを呼び出すことについても気になるな(逆の方向だけじゃなくて)。それが混合コードベースでの採用の難しい部分な気がする。最終的にはプロダクションレディを目指してるの?それとも言語デザインの探求がメインなのかな?どちらにしても、クールなプロジェクトだね。

ありがたいお言葉、ありがとうございます! :) CLIコマンドの lis run--debug フラグをサポートしていて、生成されたGoコードに //line source.lis:21:5 のディレクティブを挿入できるんです。これでランタイムエラーのスタックトレースが元のLisetteのソース位置に戻るんですよ。LSPはコンパイル時のエラーを処理していて、これは定義上 .lis ファイルを参照します。既存のGoからLisetteを呼び出すのはまだサポートされてなくて、あなたが言った通り、そっちの方向は難しいですね。頭にはあるんですが、今のところの優先事項は、ユーザーがLisetteから任意のGoのサードパーティパッケージをインポートできるようにすることです。Lisetteは探求から始まったけど、製品として使えるようにするつもりです。

正しい列挙サポートだけで満足だわ。

Goにコンパイルされる言語はいくつかあって、より良いGoを目指してるんだよね。思いつくのは、XGo (https://github.com/goplus)、Borgo (https://github.com/borgo-lang/borgo)、Soppo (https://github.com/halcyonnouveau/soppo) かな…

コンパイルエラーは、ターゲット言語からソース言語にどうやって伝播するの?

BorgoもLisetteも、(T, error)の返り値がResultの合計型と同じだと考えているようですが、これはすべてのケースで意味的に正しいわけではありません。例えば、io.ReaderインターフェースのReadメソッドは、(n!=0, io.EOF)が有効な返り値のパターンであるだけでなく、それがエラー条件ですらなく、単なる端末条件であることを指定しています。もしこの2つの返り値を相互排他的に扱うと、読み取りを停止すべきだということが見えなくなったり、バッファに有効なバイト数が置かれたことが見えなくなったりします。これは特に扱われるべきこととして十分知られているでしょうが、他のライブラリも複数の返り値の非排他性を創造的に利用していることがあります。

見た目は素晴らしいね。でも、ちょっと疑問に思うんだけど、Rustに似てるなら、機能が一致するところはRustと同じにすればいいのに。なんで "foo.bar" をインポートするの?それよりも、foo::barを使う方が良くない?Bar.Baz => の代わりに Bar::Baz => なのはなんで?何を達成しようとしてるの?Rustを知ってる人がまた別の言語を学ばなきゃいけないように、微妙に違う必要があるの?Rustを知らない人は、知識が1:1/naturallyにRustを書くのに役立たないほど違う言語を学ぶことになるの?それと、intなのにfloat64?編集:タイプミス

「(開発者が)そうしたいから」っていうのは、満足できる答えだと思う。こういう小さな言語は、実際のプロダクションで使われることを目指してるわけじゃなくて、楽しみや探求のために作られてることが多いからね。

これはただの構文の違いで、学ぶのは簡単だし、言語の主な目的ではないと思う。Rustの型システムの利点をGoにもたらすことが目的だからね。intとfloat64については、Goの数値型の名前から来てる。int、int64、float64はあるけど、floatはない。Rustにもisizeはあるけど、fsizeはないのと似てる。

同じだよ。最初はTypeScriptをベースにした高レベルのRustを書いてたけど、Rustはそんなに難しくないって気づいた。

Rustにインスパイアされてるけど、Rustを目指してないってこと?それにGoの開発者向けなんだよね?

私は言語を頻繁に切り替えていて、今はPHPを学んでるところ。構文の似てるところが逆に危険だと感じることもあるよ。「function」って見たらJavaScriptを書いてると思っちゃうけど、実際にはPHPで「+」で文字列を連結しようとして気づくんだ。「.」を使わなきゃいけないのにね。こういうのは学び始めの頃に特に感じる。

Hacker Newsで議論の続きを見る