LLMのための静的型付き関数型言語「ll-lang」を作った理由

Dev.to / 2026/4/27

💬 オピニオンDeveloper Stack & InfrastructureSignals & Early TrendsTools & Practical UsageModels & Research

要点

  • ll-langは、LLMが正しいコードをより速く生成できるようにすることに特化した、静的型付きの関数型言語であり、冗長な構文に使うトークンを減らし、実行時の驚きではなくコンパイル時のフィードバックを得られるよう設計されています。
  • 記事は、「AIコーディング」の根本的な課題はコード生成そのものではなく、主流言語が提供するフィードバックループとの不一致であり、多くの誤りが実行後に初めて表面化すると主張しています。
  • ll-langは、write→run→debug→regenerateの反復サイクルを、より早い段階で強いシグナルを得て、より多くのトークンを無駄にする前に修正できるループへ変えることを狙っています。
  • 重要な設計方針の一つはトークン効率の高い構文で(例:中括弧やセミコロンなし、キーワードは15個のみ)、同じプロンプト予算により多くのロジックを詰め込める点が強調されています。
  • このプロジェクトは、トークンに制約のあるLLMエージェント向けのツール/オーサリング言語であり、あらゆる言語を置き換えるものではないとも明確に位置づけています。

LLMが書くための静的型付け関数型言語を私たちが作った理由

ll-langは、1つの狭く実用的な仕事のための言語です。つまり、構文に使うトークン数を減らし、実行時の思いがけない事故ではなくコンパイル時のフィードバックを得ることで、LLMが正しいコードをより速く生成できるようにすることです。

「AIコーディング」の問題は、モデルがコードを生成できないことではありません。確かに生成できます。問題は、主要な多くの言語が、彼らに対して誤ったフィードバックループを与えてしまうことです。

LLMがPython、TypeScript、Javaを書いたとき、だいたい同時に2つのことが起こります。

まず、モデルがロジックをあまり運ばない構文に大量のコンテキストを消費します。中括弧、セミコロン、クラスのラッパー、繰り返しキーワード、インターフェース用のボイラープレート、儀式(手順)が多い宣言などです。実際のボトルネックがコンテキスト予算である場合、これは重要です。

次に、多くの重要なミスが遅すぎるタイミングで発覚します。モデルは、もっともらしく見えるファイルを生成し、軽く目を通せば通ってしまうかもしれませんが、実行されて初めて失敗することがあります。信号が届くのは実行時エラー、スタックトレース、あるいはアプリ側での失敗としてです。その時点では、モデルはすでに間違った道筋にトークンを費やしています。

それは高コストなループです:

write -> run -> inspect prose error -> regenerate -> run again

私たちは、そのループを変えるためにll-langを作りました。

ll-langは、LLMによるコード生成のために設計された静的型付けの関数型言語です。設計目標は意図的に狭くしています。つまり、モデルがコンパイルされるコードを書きやすくし、強力なフィードバックを素早く得られるようにしつつ、F#、TypeScript、Python、Java、C#のような通常の下流エコシステムをターゲットできるようにすることです。

これは「すべての言語を置き換える」プロジェクトではありません。トークンの圧力下でコードを書く、ある特定の制約――LLMエージェントによるコーディング――向けのツールおよびオーサリング言語です。

4つの設計原則

現在のREADMEは、ll-langを4つの原則に要約しています。製品を定義するのはこれらの組み合わせなので、ここで詳しくほどく価値があります。

1. トークン効率の良い構文

私たちは、儀式(ボイラープレート)に費やすトークンを減らして、ロジックにより多くのトークンを使える言語が欲しかったのです。

ll-langは構文をコンパクトに保ちます:

  • 中括弧がない
  • セミコロンがない
  • fntypeinthen、またはwithがない
  • キーワードは15個だけ
  • 宣言は、追加の構文ではなく、大文字/小文字の規約を使う

これは紙の上で見える以上に重要です。冗長なトークンが1つ増えるたびに、実際の推論と競合します。LLMが代数的データ型、いくつかのヘルパー、そしてパターンマッチを書かねばならないとき、コンパクトな表現と冗長な表現の差は、すぐに増幅します。

プロジェクトREADMEでは、ll-langは実コードにおいてF#より8〜17パーセントコンパクトであり、型に重い定義ではTypeScript、Python、Javaより大幅にコンパクトだと報告されています。これは美観のための選択ではありません。同じコンテキストウィンドウにどれだけ有用なロジックを詰め込めるかに直結しています。

2. 推論付きの静的型

LLM向けの言語では、モデルにすべての行へ注釈(アノテーション)を強制することはできません。そうすると、別の入口から冗長さが再導入されるだけです。

そこでll-langではHindley-Milner型推論を使います。静的型システムの保証はそのまま維持しつつ、境界を明確にするのに本当に役立つ場所でだけ注釈を書きます。残りはコンパイラが持ち運びます。

これにより、役に立つ中間地点が得られます:

  • コンパイル時の保証に十分な構造
  • モデルのための注釈オーバーヘッドが少ない
  • 宣言と実装の間でズレる可能性が減る

エージェントにとっては、どちらか一方の極端よりも良いオーサリング環境です。完全に動的なコードはミスを見つけるのが遅すぎます。注釈だらけのコードは、予算の大部分を自明な事実の説明に費やしてしまいます。

3. コンパイルできることは正しいことと同義

これはプロジェクトで最も重要な原則です。

ll-langは、LLMがいつもやりがちな種類のミスをコンパイラが捕まえるように設計されています:

  • 型の不一致
  • 未束縛変数
  • 網羅されていないマッチ
  • タグ違反
  • 単位の不一致

言い換えると、素早いフィードバックの信号は実行時ではなくコンパイル時です。

これはエージェントのワークフローにとって大きな転換です。モデルに、プログラム全体を頭の中でシミュレートしてから実行時の失敗を解釈させるのではなく、よりタイトなループに保てます:

write -> check -> fix one precise error code -> continue

これはより安価で、より速く、そして自動化もしやすいです。

4. LLMが読めるエラー

診断が、エージェントが読み取らなければならない長い人間向けの文章として書かれているなら、強力な型システムであっても有用性が下がります。

ll-langのエラーは、意図的にコンパクトで機械可読になるようにしています:

EXXX line:col ErrorKind details

READMEにある例には次のものがあります:

  • E001 12:5 TypeMismatch Str Str[UserId]
  • E003 15:1 NonExhaustiveMatch Shape missing:Empty
  • E004 20:9 UnitMismatch Float[m] Float[s]
  • E005 7:14 TagViolation Str[Email] Str[UserId]

これは、エージェントがまずコードで振り分け、次にテキストを見ることができるため重要です。何が間違っていたのかを理解するのに、壊れやすい自然言語パーサーは必要ありません。

実例:実行時の前に本当のバグを見つける

AI生成コードで常に出てくる種類のバグを示します。モデルが、生の文字列、タグ付き識別子、そして合成すべきではない計測値を取り違えてしまうというものです。

動的言語では、それがアプリが動くまで生き残ってしまうことがよくあります。ll-langでは、すぐに拒否されます。

module ResetFlow

tag UserId
tag Email
tag m
tag s

lookupEmail(id Str[UserId]) Str[Email] = "alice@example.com"[Email]
sendReset(to Str[Email]) = to

bad(rawId Str)(distance Float[m])(elapsed Float[s]) =
  email = lookupEmail rawId
  total = distance + elapsed
  sendReset rawId

ここには3つの明確なミスがあります:

  1. lookupEmailStr[UserId]を期待していますが、rawIdはただのStrです。
  2. distance + elapsedはメートルと秒を足そうとしています。
  3. sendResetStr[Email]を期待していますが、生の文字列が渡されています。

これらは例外ケースではありません。モデルが複数の概念を同時に扱っているときに、呼び出し箇所(コールサイト)で1つの意味的な詳細を見失って起きる、まさにその種のミスです。

ll-langでは、コンパイラがそれらを一次のフィードバックとして捕まえます。エラーストリームは後回しのものではありません。製品そのものです。

次のような正確なシグナルが得られます:

  • E005 TagViolation:タグが付いていない文字列を渡し、Str[UserId]またはStr[Email]が必要なのに満たせなかった場合
  • E004 UnitMismatch:互換性のある単位を共有していない値を組み合わせた場合

修正は明示的で局所的です:

module ResetFlow

tag UserId
tag Email
tag m
tag s

lookupEmail(id Str[UserId]) Str[Email] = "alice@example.com"[Email]
sendReset(to Str[Email]) = to
speed(distance Float[m])(elapsed Float[s]) = distance / elapsed

good(rawId Str)(distance Float[m])(elapsed Float[s]) =
  userId = rawId[UserId]
  email = lookupEmail userId
  rate = speed distance elapsed
  sendReset email

この違いが、「コンパイルできることは正しいことと同義」が単なるスローガンではなく、より良いエージェントのループの基盤である理由そのものです。

モデルがミスをするのは避けられません。だからこそ、そのミスは実行時の振る舞いの遅延として現れるのではなく、コンパクトなコンパイラ診断へと収束してほしいのです。

このことが特にLLMにとって重要な理由

返却形式: {"translated": "翻訳されたHTML"}

人間はしばしば弱い信号を補うことができます。行間を読み、スタックをたどり、コードベースのどこかで使われている慣習を思い出し、システムがたぶん何を意図していたのかを推測します。

LLMは異なります。LLMは、次の条件がそろっているときにずっと得意です:

  • 構文が規則的である
  • エラーの形が安定している
  • 修復対象がローカルである
  • フィードバックが実行前に到着する

ll-langは、人間がコードを書くのと同じやり方でモデルがコードしているふりをするのではなく、その現実に寄り添います。

優れたLLMオーサリング言語は、順調な(ハッピーパスの)道を簡単にすべきですが、失敗の道も判読可能にしなければなりません。ll-langは、そこに設計予算のほとんどを費やしています。

自己ホストは、ブランディングではなく証明

多くの言語プロジェクトは、初期の段階で野心的な主張をします。私たちは、偽造しにくいものが欲しかったのです。

ll-langは自己ホストです。コンパイラのパイプラインはll-langで実装されており、レクサ、パーサ、エラボレータ、型推論、コード生成、モジュールシステム、そしてMCPサーバが含まれます。READMEにあるブートストラップの不動点の例は、成熟度を強く示す主張です:

compiler1.fs == compiler2.fs

これは重要です。なぜなら、同時にいくつかのことが証明されるからです:

  • その言語は、おもちゃの例だけでなく本物のコンパイラ作業を扱える
  • 標準ライブラリとモジュールシステムは、意味のある規模で使える
  • コアとなるセマンティクスを壊す変更が、その言語自身のビルドループの中で顕在化する

DevRelにとっても、自己ホストは物語を「面白い実験」から「運用上の証拠を伴う稼働するシステム」へと変えます。

MCP層が、コンパイラをエージェント用ツールに変える

言語だけでは話の半分にすぎません。残りの半分は、エージェントがそれとどう相互作用するかです。

ll-langはlllc mcp経由でMCPサーバを同梱しています。つまりClaude Code、Cursor、Zed、そしてその他のMCP対応クライアントは、シェルに投げてターミナル出力をかき集めるのではなく、コンパイラ機能を構造化されたツールとして呼び出せます。

現在のREADMEとユーザーガイドには、次の30のツールが文書化されています:

  • コンパイルとチェックのフロー
  • 診断と修復ヘルパ
  • フォーマットとASTの検査
  • プロジェクトグラフとビルド操作
  • シンボルナビゲーション
  • 依存関係ヘルパ
  • テストヘルパ
  • FFIヘルパ
  • カタログとメタデータのルックアップ

これは重要です。なぜなら、モデルが構造化されたループの中にとどまれるからです:

  1. ソースを書く
  2. check_sourceまたはcheck_fileを呼び出す
  3. 構造化された診断を検査する
  4. lookup_errorまたは修復指向のツールを呼び出す
  5. 再試行する

これは、「エージェントに対して、シェルコマンドが何を意味していたかを推測させる」よりも、はるかにきれいなアーキテクチャです。

なぜTypeScriptやPythonを、より良いプロンプトで使わないのか?

プロンプトは役に立ちますが、根本の信号問題は解決しません。

メインストリームの言語でも、有用なコードを得ることは確実にできます。ですが、LLMによって書かれるロジックという特定のケースでは、依然としてトレードオフが発生します:

  • 構文のオーバーヘッドが増える
  • 一般的なワークフローでのコンパイル時の保証が弱い
  • ランタイムの失敗がよりノイジーになる
  • エラーメッセージがエージェント向けではなく人向けに最適化されている

ll-langは、プロンプトにすべての負担を背負わせるのではなく、デフォルトの環境自体を変えます。

ll-langはどこに収まるのか

ll-langは、次の場合に適しています:

  • LLMエージェントが型付きのビジネスロジックを生成している
  • フレームワークの幅よりもコンパイル時の正確さがより重要である
  • 同じソースを複数のランタイムに向けてターゲットにする必要がある
  • トークン効率が現実的な制約である

あらゆるプログラミング問題への答えになろうとしているわけではありません。より狭い用途のための、より鋭いツールです。

それはたいてい良い兆候です。

試してみる

リポジトリ: https://github.com/Neftedollar/ll-lang
ランディングページ: https://neftedollar.com/ll-lang/

現在のREADMEからのブートストラップ手順:

git clone https://github.com/Neftedollar/ll-lang.git
cd ll-lang
LLLC_BOOTSTRAP_REINSTALL=1 ./tools/check-selfhost-ci.sh

エージェントの物語を直接見たい場合は、MCPサーバをクライアントに接続してください:

{
  "mcpServers": {
    "ll-lang": {
      "command": "lllc",
      "args": ["mcp"]
    }
  }
}

MCPサーバのラベルは任意です。いくつかのドキュメントではlllcになっており、他ではll-langになっています。重要なのはcommand: "lllc"args: ["mcp"]であることです。

そして、エディタまたはエージェントに簡単な質問をします:「これ、コンパイルできる?」

これが、ll-langの背後にあるコアの賭けです。儀式の少ない言語、より強い保証、より良い診断をモデルに与えることで、コーディングループの品質が実質的に改善されます。

人間を中心にした言語では、真実が現れるのはしばしばランタイムです。

エージェントを中心にしたワークフローでは、それでは遅すぎます。