Skip to content
Published on

小さな言語の魅力 — Janet、Luaと組み込みスクリプティングの世界

Authors

はじめに — なぜ今、再び小さな言語なのか

2026年6月、Ian Henry氏のエッセイ「Why Janet」が再びHacker Newsの上位に上がりました。公開から数年経った記事が再び召喚された背景には、明確な時代的文脈があります。巨大な言語エコシステムへの疲労感です。

最近の開発者の日常を思い浮かべれば理解できます。Node.jsプロジェクトを1つ始めればnode_modulesに数百個の依存関係がインストールされ、2026年6月にはnpmサプライチェーン攻撃がRed Hat Cloud Servicesの内部にまで侵入したというニュースが流れました。Pythonの環境は仮想環境とパッケージングツールの迷路であり、Rustは強力ですがコンパイル時間と学習曲線は決して軽くありません。AIコーディングエージェントがボイラープレートを代わりに書いてくれる時代になると、逆説的に「自分が全部理解できる小さなツール」への渇望が大きくなりました。

小さな言語はその渇望への1つの答えです。言語仕様全体を週末に読み切れて、ランタイムが単一バイナリ1つで、依存関係グラフが一目で把握できる世界。この記事では、組み込み可能スクリプティング言語の代表格であるLuaと新興勢力のJanetを中心に、小さな言語が何を与えてくれるのかを見ていきます。

組み込み可能言語とは何か

組み込み可能(embeddable)言語とは、単独実行よりもホストアプリケーションの中に内蔵されて動作することを第一の目標として設計された言語です。主な特徴は次の通りです。

  • ランタイムがCライブラリの形で提供され、ホストプログラムにリンクできる
  • ホストとスクリプトの間で値をやり取りする明確なAPI(バインディングインターフェース)がある
  • ランタイムのサイズが小さく(数百KB程度)、起動時間が短い
  • メモリ使用量と実行をホストが制御できる(サンドボックス化、リソース制限)

典型的な構造を図にするとこうなります。

+--------------------------------------------------+
|        ホストアプリケーション (C/C++/Rust)           |
|                                                  |
|  +--------------------+   +-------------------+  |
|  |   コアエンジン        |   |  スクリプトVM      |  |
|  |  (性能クリティカル)    |<->|  (Lua / Janet)    |  |
|  |  レンダリング、IO、物理 |   |  ゲームロジック、設定 |  |
|  +--------------------+   |  プラグイン、拡張    |  |
|                           +-------------------+  |
|      C API境界: スタック/レジストリベースの値交換      |
+--------------------------------------------------+

この構造の利点は明確です。性能が重要な部分はホスト言語で、頻繁に変わり柔軟であるべき部分はスクリプトで書きます。ゲーム業界は数十年にわたりこのパターンで開発速度と実行性能を両立させてきました。

Lua — 組み込み可能言語の教科書

Luaは1993年にブラジルのPUC-Rio大学で生まれ、30年以上にわたり組み込み可能言語の標準として君臨してきました。成功事例のリストはそのままソフトウェア産業の断面図です。

  • ゲーム: World of Warcraftのアドオン、Roblox(派生言語Luau)、Garry's Mod、Defoldなど数多くのゲームエンジンのスクリプティングレイヤー
  • Webインフラ: nginxベースのOpenRestyはリクエスト処理ロジックをLuaで書けるようにし、Cloudflareが初期のエッジロジックをこのスタックで運用していたことで有名です
  • データベース: RedisのEVALコマンドはLuaスクリプトでアトミックな操作を実行します
  • エディタ: Neovimは設定とプラグインの言語としてLuaを採用し、VimScriptを事実上置き換えました

Luaがこれほど広く使われる理由は設計哲学にあります。インタープリタ全体が約3万行のANSI Cで書かれており、外部依存がなく、コンパイルすると数百KBのライブラリができあがります。言語自体も小さいのです。データ構造は実質的にテーブル1つだけで、このテーブルで配列、ハッシュマップ、オブジェクト、モジュールをすべて表現します。

Lua基本文法の味見

-- 変数とテーブル: Luaの唯一の複合データ構造
local config = {
  name = "my-server",
  port = 8080,
  tags = { "web", "production" },
}

-- 関数は第一級市民
local function greet(name)
  return "Hello, " .. name .. "!"
end

print(greet(config.name))

-- テーブルの走査
for i, tag in ipairs(config.tags) do
  print(i, tag)
end

-- メタテーブルでオブジェクト指向を真似る
local Animal = {}
Animal.__index = Animal

function Animal.new(name, sound)
  local self = setmetatable({}, Animal)
  self.name = name
  self.sound = sound
  return self
end

function Animal:speak()
  print(self.name .. " says " .. self.sound)
end

local dog = Animal.new("Rex", "woof")
dog:speak()  -- Rex says woof

文法がシンプルだということは学びやすいという意味でもありますが、より重要なのは実装が小さく速いという意味です。さらにLuaJITという傑出したJITコンパイラまであり、特定のワークロードではCに迫る性能を出します。

CからLuaを組み込む

LuaのC APIはスタックベースです。ホストとVMが仮想スタックを通じて値をやり取りします。

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <stdio.h>

/* C関数をLuaに公開: 2つの数を足す */
static int l_add(lua_State *L) {
    double a = luaL_checknumber(L, 1);
    double b = luaL_checknumber(L, 2);
    lua_pushnumber(L, a + b);
    return 1;  /* 戻り値の個数 */
}

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    /* C関数の登録 */
    lua_register(L, "add", l_add);

    /* Luaコードの実行 */
    if (luaL_dostring(L, "print('from lua:', add(3, 4))") != LUA_OK) {
        fprintf(stderr, "error: %s\n", lua_tostring(L, -1));
    }

    /* Luaのグローバル変数を読む */
    luaL_dostring(L, "answer = 42");
    lua_getglobal(L, "answer");
    printf("answer from lua = %lld\n", (long long)lua_tointeger(L, -1));

    lua_close(L);
    return 0;
}

コンパイルは次の1行で済みます。

cc host.c -o host -llua -lm

50行未満のコードでホストとスクリプトが双方向に対話します。この参入障壁の低さこそ、Luaが30年生き残った秘訣です。

Janet — 小さなLispの再発見

JanetはCalvin Rose氏(Fennel言語の原作者でもあります)が作ったLisp系言語です。Ian Henry氏の「Why Janet」と彼の無料電子書籍「Janet for Mortals」が入門経路としてよく推薦されます。Janetの魅力を整理すると次の通りです。

  • 単一バイナリ: インタープリタ、コンパイラ、REPLが1MB前後の実行ファイル1つに収まっています
  • Cアマルガメーション: ランタイム全体がjanet.cとjanet.hのわずか2ファイルで配布され、プロジェクトにコピーするだけで組み込みが始まります
  • PEG内蔵: 正規表現の代わりにPEG(Parsing Expression Grammar)モジュールが標準ライブラリに含まれており、パーサーを宣言的に書けます
  • イベントループとファイバー: 軽量並行性が言語レベルでサポートされています
  • ネイティブ実行ファイル生成: jpmビルドツールでスクリプトを静的バイナリにまとめられます

Janet基本文法の味見

# JanetはLispですが、角括弧/波括弧リテラルがあって読みやすい
(def config
  {:name "my-server"
   :port 8080
   :tags ["web" "production"]})

(defn greet [name]
  (string "Hello, " name "!"))

(print (greet (config :name)))

# シーケンス処理: 関数型スタイル
(def doubled (map (fn [x] (* 2 x)) [1 2 3 4 5]))
(pp doubled)  # @[2 4 6 8 10]

# 可変/不変データ構造の両方が標準装備
(def immutable-tuple [1 2 3])
(def mutable-array @[1 2 3])
(array/push mutable-array 4)

# ファイバーでジェネレータを作る
(def counter
  (coro
    (for i 0 3
      (yield i))))
(each n counter (print n))  # 0 1 2

Janetの秘密兵器 — PEG

正規表現が限界にぶつかる瞬間(ネスト構造、再帰パターン)にPEGは輝きます。シンプルなkey=value設定ファイルのパーサーをPEGで書くとこうなります。

(def config-grammar
  ~{:ws (any (set " \t"))
    :key (capture (some (range "az" "AZ" "09" "__")))
    :value (capture (some (if-not "\n" 1)))
    :line (* :ws :key :ws "=" :ws :value)
    :main (some (* (+ :line :ws) (+ "\n" -1)))})

(def result (peg/match config-grammar "host = localhost\nport = 8080\n"))
(pp result)
# => @["host" "localhost" "port" "8080"]

文法自体がデータ構造(テーブル)なので、合成と再利用が容易です。正規表現の文字列を繋ぎ合わせて苦しんだ経験があれば、この宣言的スタイルの価値をすぐに実感できます。

CからJanetを組み込む

Janetの組み込みはLuaよりもさらにシンプルだと評されています。アマルガメーションファイルを取得して一緒にコンパイルすれば終わりです。

#include "janet.h"
#include <stdio.h>

/* C関数をJanetに公開 */
static Janet cfun_add(int32_t argc, Janet *argv) {
    janet_fixarity(argc, 2);
    double a = janet_getnumber(argv, 0);
    double b = janet_getnumber(argv, 1);
    return janet_wrap_number(a + b);
}

static const JanetReg cfuns[] = {
    {"native-add", cfun_add, "(native-add a b)\n\n2つの数を足す。"},
    {NULL, NULL, NULL}
};

int main(void) {
    janet_init();
    JanetTable *env = janet_core_env(NULL);
    janet_cfuns(env, "host", cfuns);

    Janet out;
    janet_dostring(env,
        "(print \"from janet: \" (native-add 3 4))",
        "main", &out);

    janet_deinit();
    return 0;
}
cc host.c janet.c -o host -lm -lpthread

ファイルを2つコピーしてコンパイラを1回呼び出すだけで組み込みが完了します。ビルドシステムとの戦いがないこと、これが小さな言語の実用的価値です。

小さな言語が与えてくれるもの

小さな言語を擁護する論拠は感傷ではなく、工学的な実利です。

全体理解可能性

Luaのリファレンスマニュアルは半日あれば精読できます。Janetのドキュメントも同様です。言語のすべての動作を頭に入れられるということは、デバッグの際に「言語が変なことをした可能性」を排除し、自分のコードに集中できるという意味です。巨大な言語では、コンパイラの特殊ケース、標準ライブラリの微妙な動作、ビルドツールの魔法までもが疑いの対象になります。

高速な起動と短いフィードバックループ

REPLの起動に数十ミリ秒、スクリプトの実行も即座です。巨大フレームワークのコールドスタートを待ちながら集中力が途切れることはありません。

少ない依存関係、小さな攻撃対象領域

2026年のnpmサプライチェーン攻撃の事態が示すように、依存関係の1つ1つが攻撃対象領域です。標準ライブラリだけでほとんどを解決する小さな言語は、サプライチェーンリスク自体が構造的に小さいのです。

設定言語としての活用 — YAML地獄からの脱出

小さな言語のもう1つの活用先は設定です。YAMLはインデントの罠、暗黙の型変換(ノルウェー問題として知られるnoキーワードのブール解釈など)、繰り返し表現の欠如で悪名高いです。数百行のHelm valuesファイルやCI設定をコピー&ペーストで管理した経験のある人なら共感するでしょう。

スクリプト言語を設定に使えば、変数、関数、条件文をそのまま活用できます。

-- config.lua: プログラマブルな設定
local base = {
  image = "myapp",
  replicas = 2,
}

local envs = {}
for _, name in ipairs({ "dev", "staging", "prod" }) do
  local cfg = {}
  for k, v in pairs(base) do cfg[k] = v end
  cfg.namespace = "myapp-" .. name
  if name == "prod" then cfg.replicas = 6 end
  envs[name] = cfg
end

return envs

繰り返しと分岐が言語機能なので、アンカー/マージキーのようなYAMLの曲芸は不要です。実際、Neovimコミュニティがinit.vimからinit.luaへ大移動したのも同じ動機でした。設定がコードになれば、検証、モジュール化、再利用が自然になります。

DSL設計入門 — 小さな言語の上にさらに小さな言語を

組み込み可能言語はDSL(ドメイン特化言語)の土台としても優れています。アプローチは大きく2つあります。

  1. 内部DSL: ホストスクリプト言語の文法の中でドメイン語彙を設計します。Lisp系のJanetはマクロのおかげでこの方式が特に強力です。
  2. 外部DSL: 独自の文法を定義してパーサーを書きます。Janetの内蔵PEGがこの参入障壁を大きく下げてくれます。

Janetマクロで作ったミニ内部DSLの例です。HTTPルーティングテーブルを宣言的に定義します。

(defmacro defroutes [name & routes]
  ~(def ,name
     ,(map (fn [[method path handler]]
             {:method method :path path :handler handler})
           routes)))

(defroutes app-routes
  [:get "/users" list-users]
  [:post "/users" create-user]
  [:get "/health" health-check])

# マクロ展開の結果: 普通のデータ構造の配列
# ルーターの実装はこの配列を走査するだけです

マクロがコンパイルタイムにコードをデータに変換してくれるので、ランタイムコストなしでドメイン語彙が得られます。「設定のように読めるが実はコード」であるインターフェースを作ること、それが内部DSLの真髄です。

その他の候補たち — 組み込み可能言語エコシステムの地図

LuaとJanet以外にも、見ておく価値のある小さな言語があります。

  • Wren: 「Game Programming Patterns」と「Crafting Interpreters」の著者Bob Nystrom氏が作ったクラスベースのスクリプティング言語。文法が親しみやすく、ファイバーベースの並行性をサポートし、実装コードが読みやすいことで有名です
  • Gravity: Marco Bambini氏がCreo開発環境のために作った言語で、Swiftに似た文法が特徴です。Cで書かれた小さなランタイムを提供します
  • Fennel: 新しいランタイムではなく、LuaにコンパイルされるLispです。Luaエコシステム(LuaJIT、Neovim、ゲームエンジン)をそのまま使いながら、Lispの文法とマクロが手に入ります
  • Rhai: Rustエコシステムのための組み込みスクリプティング言語。Rustの型との統合がスムーズで、デフォルトがサンドボックス志向(演算回数制限など)なので安全なユーザースクリプティングに適しています
  • mruby: Rubyの軽量な組み込み可能実装。Ruby文法を好むチームの選択肢になります
  • Squirrel: ゲーム業界で長く使われてきた言語で、Valveの一部タイトルに採用された歴史があります

選択基準テーブル

どの言語を組み込むか選ぶ際に参考になる比較表です。

基準LuaJanetWrenRhaiFennel
文法系統手続き型LispクラスベースRust類似Lisp
ランタイムサイズ非常に小さい小さい非常に小さい小さいLuaに依存
ホスト言語CCCRustLuaランタイム
JITの選択肢LuaJITなしなしなしLuaJIT
エコシステム規模非常に大きい小さいが活発小さいRust内で成長中Luaと共有
並行性コルーチンファイバー、イベントループファイバーホストに委譲コルーチン
パーシングツール外部ライブラリPEG内蔵外部外部Lua資産を活用
実績のある分野ゲーム、インフラCLI、スクリプティングゲーム、学習Rustアプリ拡張Neovim、ゲーム

実務観点の要約ガイドは次の通りです。

  • 実績のあるエコシステムと性能(LuaJIT)が最優先ならLua
  • 単一ファイル組み込み、PEG、Lispマクロに魅力を感じるならJanet
  • ホストがRustならRhaiが統合コスト最小
  • すでにLuaベースのホスト(Neovimなど)を使っているならFennelで文法だけアップグレード

プロダクション導入時の考慮事項

小さな言語をユーザー向けプロダクトに入れる際は、2つのことを深く検討する必要があります。

サンドボックス化

ユーザーが書いたスクリプトを実行するなら、隔離は必須です。チェックリストは次の通りです。

サンドボックス化チェックリスト
[ ] 危険なモジュールの遮断: ファイルIO、プロセス実行、
    ネットワークアクセスを環境から除去したか
    (Luaならosとioテーブルを除去)
[ ] CPU暴走の防御: 無限ループを止める手段があるか
    (Luaデバッグフックのinstruction count、Rhaiの演算制限、
     別スレッド + タイムアウトなど)
[ ] メモリ制限: カスタムアロケータやGC上限で暴食を防ぐか
[ ] スタック深度制限: 再帰爆弾でホストが死なないか
[ ] エラー境界: スクリプトのエラーがホストのクラッシュに
    波及しないか (protected call経由で実行)

Luaは環境テーブルを制限する方式のサンドボックス化が慣行として確立しており、Rhaiは最初から制限実行を念頭に設計されています。どちらにせよ「デフォルト環境のままユーザーコードを実行」は禁物です。

性能

インタープリタ言語の呼び出し境界コストを理解する必要があります。

  • ホスト-スクリプト境界の通過回数を減らすことが最優先の最適化です。ピクセル単位でスクリプトを呼べばどんな言語でも遅くなります。フレーム単位、イベント単位で粗く呼びましょう
  • Luaがボトルネックの場合はLuaJITへの切り替えを検討します。ただし、LuaJITはLua 5.1互換であること、メンテナンス体制が本家と異なることを併せて評価する必要があります
  • JanetにはJITがないため、数値演算ループはC関数に下ろす設計が定石です
  • GCの一時停止が問題なら、インクリメンタルGC設定(Lua)や手動GC呼び出しタイミングの制御をチューニングします

落とし穴と反論 — 大きな言語エコシステムとのバランス

小さな言語の礼賛論には、必ず押さえるべき反論があります。

第一に、エコシステムの不在は実質的なコストです。JanetでWebサービスを作れば、認証ライブラリ、ORM、クラウドSDKを自分で書くか、Cバインディングで埋める必要があります。標準ライブラリが届かない領域では、「全部理解できる」の対価は「全部自分で実装する」です。

第二に、採用とオンボーディングの問題です。チームにJanet経験者がいる確率は低いでしょう。言語が小さいので学習は速いですが、イディオムとベストプラクティスの蓄積が浅いことはコードレビューの品質に影響します。

第三に、AIツールとの相性も現実的な変数です。2026年現在、コーディングエージェントは学習データが豊富なPython、TypeScriptで最も強力です。マイナー言語ではハルシネーションが頻発し、AI支援の生産性という大きな言語の新しい利点が、小さな言語の相対的な弱点になりました。ただし、言語が小さければエージェントに言語仕様全体をコンテキストとして渡せるという逆転の発想も可能です。CLAUDE.mdにJanetのチートシートを入れておくといったコンテキストエンジニアリングは実際に効果があります。

結論は二分法ではありません。プロダクトのコアは主流言語で、拡張ポイントと設定とドメインロジックは小さな組み込み言語で構成するハイブリッドこそ、Luaの30年史が証明したバランスポイントです。

実務適用ガイド — 今週末から始める

小さな言語を体験する段階的な経路を提案します。

  1. Janetをインストールし、REPLで30分遊んでみる。公式ドキュメントのチュートリアルで十分です
# macOS
brew install janet
janet -e '(print "hello, small world")'

# ソースからのビルドも簡単です
git clone https://github.com/janet-lang/janet.git
cd janet && make && sudo make install
  1. 日常のスクリプトを1つJanetかLuaで書いてみる。ログのパースならJanet PEGの真価を感じられます
  2. C組み込みミニプロジェクト: 上記の例のコードをもとに、自分のプログラムにスクリプトフックを1つ開けてみる
  3. 設定ファイルを1つLuaに移行してみる: 繰り返しの多いYAMLが良い候補です
  4. 深く掘り下げたいなら「Crafting Interpreters」を読み、自分で小さな言語を作ってみる。Wrenの作者の本なので、組み込み可能設計のセンスをそのまま学べます

おわりに

「Why Janet」が再び話題になる現象は、単なる懐古ではありません。依存関係の疲労、サプライチェーンの不安、ツールの複雑さの蓄積の中で、全部理解できる小さなツールの価値が再評価されているというシグナルです。

小さな言語は大きな言語を置き換えません。その代わり、大きなシステムの隙間 — 設定、拡張、DSL、ユーザースクリプティング — で複雑さを吸収する緩衝材の役割を果たします。Luaがゲームとインフラで証明し、Janetがより現代的なツールでその系譜を継いでいます。週末の1日を投資してREPLを立ち上げてみてください。言語を丸ごと1つ理解するという感覚は、思った以上に大きな喜びです。

参考資料