Upgrade to Pro — share decks privately, control downloads, hide ads and more …

言語を使う側から、作る側へ。 自作 Lisp で得た新たな気づき。

言語を使う側から、作る側へ。 自作 Lisp で得た新たな気づき。

Tomoya Nakabayashi GitHub: borashisan
2026 年 7 月 13 日
KaigiEffect Kaigi 2026

Avatar for ANDPAD inc

ANDPAD inc

July 14, 2026

More Decks by ANDPAD inc

Other Decks in Programming

Transcript

  1. 3 選手入場 | 自己紹介 フットボールとポケモン好きのSWE。 中林友弥 / @borashisan ▪2022年新卒からRuby/Rails、現在はアンドパッドの SWE

    ▪大阪出身。関西弁のくせにボケもツッコミも放棄 ▪TOEIC 860点(ただし英会話は苦手) ▪好き: フットボール(米語: サッカー)・ポケモン
  2. 選手入場 | 自作Lispの中身 純lispでインタプリタ、作れます! 1 Lexer (8行) 文字列 → トークン

    ▶ 2 Parser (37行) トークン → AST ▶ 3 Evaluator (62 行) ASTを実行 環境・組み込み関数まで全部足しても、Ruby 約230行 特殊形式は5つ quote define if cond lambda 組み込みは15個 car cdr cons 四則演算・比較 など これだけでチューリング完全。マンデルブロ集合だって描ける。 5 ※この設計はMcCarthyの「純Lisp(Pure Lisp)」を参考にしています。 Wikipedia: https://ja.wikipedia.org/wiki/Lisp
  3. 選手入場 | (参考)マンデルブロ集合を記述するlisp マンデルブロ集合を記述するlisp(scheme方言) mandelbrot.scm ;; 1. 定数と基本関数 (define SCALE

    1000000) (define quotient (lambda (a b) (if (< a 0) (- 0 (quotient (- 0 a) b)) (if (< a b) 0 (if (< a (+ b b)) 1 ((lambda (q) (+ (+ q q) (quotient (- a (* (+ q q) b)) b))) (quotient a (+ b b)))))))) (define fix* (lambda (a b) (quotient (* a b) SCALE))) ;; 2. 文字マッピング (define char-for-count (lambda (count max-iter) ((lambda (q3) (cond ((eq? count max-iter) '@) ((> count (+ q3 q3)) '*) ((> count q3) '+) (else '-))) (quotient max-iter 3)))) mandelbrot.scm ;; 3. マンデルブロ集合計算の核心 (define mandelbrot-iter (lambda (cr ci zr zi count max-iter) ((lambda (zr2 zi2 zri) (if (>= (+ zr2 zi2) (* 4 SCALE)) count (if (>= count max-iter) count (mandelbrot-iter cr ci (+ (- zr2 zi2) cr) (+ (* 2 zri) ci) (+ count 1) max-iter)))) (fix* zr zr) (fix* zi zi) (fix* zr zi)))) (define mandelbrot (lambda (cr ci max-iter) (mandelbrot-iter cr ci 0 0 0 max-iter))) ;; 4. 描画とメイン処理 (define print-row (lambda (x y x-end step max-iter) (if (> x x-end) (newline) ((lambda (_) (print-row (+ x step) y x-end step max-iter)) (display (char-for-count (mandelbrot x y max-iter) max-iter)))))) (define render (lambda (y y-end x-start x-end step max-iter) (if (> y y-end) 'done ((lambda (_) (render (+ y step) y-end x-start x-end step max-iter)) (print-row x-start y x-end step max-iter))))) ;; 実行 (render -1000000 1000000 -2000000 500000 50000 30)
  4. 選手入場 | (参考)マンデルブロ集合を記述するlisp terminal $ ruby bin/repl.rb examples/mandelbrot.lisp -------------------------------------+-+@---------- --------------------------------------*------------

    -------------------------------------@*+----------- ----------------------------------+@+*@++---------- -----------------------------------+@@@@+---------- -----------------------------------*@@@@@---------- -----------------------------------+@@@@+------+--- ----------------------------+@+-@+@@@@@@*@+@---+--- ----------------------------+@@+*@@@@@@@@@@++++++-- -----------------------------@@@@@@@@@@@@@@@@@@*--- ---------------------------++@@@@@@@@@@@@@@@@@@+--- --------------------------*@*@@@@@@@@@@@@@@@@@@+--- ---------------+----------++@@@@@@@@@@@@@@@@@@@+-+- ---------------+---+------@@@@@@@@@@@@@@@@@@@@@@@+- ----------------++++@++--+*@@@@@@@@@@@@@@@@@@@@@+-- ----------------+@*@@@*+++@@@@@@@@@@@@@@@@@@@@@@+-- ---------------++@@@@@@@++@@@@@@@@@@@@@@@@@@@@@@@-- ------------+--+@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@*-- ------------+++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@--- ------------+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@---- -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+---- ------------+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@---- ------------+++*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@--- ------------+--+@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@*-- ---------------++@@@@@@@++@@@@@@@@@@@@@@@@@@@@@@@-- ----------------+@*@@@*+++@@@@@@@@@@@@@@@@@@@@@@+-- ----------------++++@++--+*@@@@@@@@@@@@@@@@@@@@@+-- ---------------+---+------@@@@@@@@@@@@@@@@@@@@@@@+- ---------------+----------++@@@@@@@@@@@@@@@@@@@+-+- --------------------------*@*@@@@@@@@@@@@@@@@@@+--- ---------------------------++@@@@@@@@@@@@@@@@@@+--- -----------------------------@@@@@@@@@@@@@@@@@@*--- ----------------------------+@@+*@@@@@@@@@@++++++-- ----------------------------+@+-@+@@@@@@*@+@---+--- -----------------------------------+@@@@+------+--- -----------------------------------*@@@@@---------- -----------------------------------+@@@@+---------- ----------------------------------+@+*@++---------- -------------------------------------@*+----------- --------------------------------------*------------ -------------------------------------+-+@---------- 実行結果 自作Lisp上で描画された マンデルブロ集合 Lexer / Parser わずか 45 行 Evaluator 約 100 行 わずか45行のLexer/Parserと、約100行のEvaluatorで構築されたイ ンタープリタ上で実行。 再帰計算により、見事なテキストアートとしてマンデルブロ集合を出 力します。 実行環境 自作 Lisp インタプリタ (Schemeサブセット)
  5. 計算のシャトルラン 01 準備 初期値の設定 全ピクセルそれぞれの座標を処理 の「スタート地点」として設定し ます。 02 耐久ループ(最大30回) 現在地

    = (現在地²)+スタート もし計算途中で値が枠(2.0)を 飛び出したら: ➔ その回数に応じた「色」を塗っ てループを終了。 03 判定 ループを耐え抜いた場合 30回のループを飛び出さずに耐え 抜いたピクセルには、 「黒(空白)」を塗る 🚨 なんでマンデルブロ集合で計測するの 前回の結果を次へ渡す計算を、全ピクセルで何十回もひたすら回します。なので「最適化の力」が露骨に実行パフォー マンスへと影響します。 15 選手入場 | (参考)マンデルブロ集合って何をやってるの
  6. 合わせて、わずか 45行。 1. Lexer (字句解析器) # カッコの前後に空白を入れて、splitするだけ def self.tokenize(input) input.gsub('(',

    ' ( ').gsub(')', ' ) ').split end 2. Parser (構文解析器) # "(" が来たら ")" まで再帰でリストを組み立てる def parse(tokens) token = tokens.shift if token == '(' list = [] list << parse(tokens) while tokens.first != ')' tokens.shift # ')' を捨てる list else atom(token) # 数値ならInteger、他はSymbolに end end 入力コード (文字列) "(+ 1 2)" ▶ 1. Lexer出力 (トークン列) ["(", "+", "1", "2", ")"] ▶ 2. Parser出力 (AST / リスト) [:+, 1, 2] 6 選手入場 | Lexer & Parser
  7. 1. eval (メインループ) def eval(exp, env) case exp when Integer

    then exp # 数値 → そのまま値 when Symbol then env.get(exp) # 変数 → 環境 から探す when Array then evaluate_list(exp, env) # 式 として実行 end end 2. evaluate_list (リストの評価) def evaluate_list(exp, env) operator, *args = exp case operator when :quote, :define, :if, :cond, :lambda # 特殊形式 ... else # あとは全部「関数適用」 procedure = eval(operator, env) procedure.call(args.map { |arg| eval(arg, env) }) end end 心臓部も、case文。 7 選手入場 | Evaluator
  8. 自作Lisp (Ruby) 11.33 s Tree-walking・最適化ゼロ その差、約79倍。 8 選手入場 | パフォーマンス比較

    Gauche (Scheme) 0.14 s VMバイトコード実行 ※ マンデルブロ集合の描画時間(同一マシン・5回計測の中央値。timeコマンドの5回計測のうちの1回の生ログ出力) $ time gosh examples/mandelbrot.lisp ---------------------+-+@---------- 0.15s user 0.02s system 132% cpu 0.123 total #5回計測のうちの 1回 $ time ruby bin/repl.rb examples/mandelbrot.lisp ---------------------+-+@---------- 11.09s user 0.10s system 98% cpu 11.398 total #5回計測のうちの 1回
  9. 10 前半 | 犯人はTree-walking eval 毎回、やり直してる。 01 構文判定 「ifか?lambdaか?」 arrow_

    02 環境探索 変数を探し回る arrow_ 03 Proc呼び出し 関数を呼ぶ たった1つの足し算 (+ a b) でも…… 環境探索 ×3回 + と a と b を探す 使い捨てArray ×2個 式の分解と引数リスト メソッド呼び出し ×6回 evalの再帰・Proc経由 ↑ マンデルブロ集合だと描画のあいだ、何千万回もくり返される(O(10^7))
  10. 11 前半 | イエローカー ド どうやって最適化する?(AIに聞くと) # 最初の作戦 : 演算子を評価器にハードコードする

    (Fast-path) when :+ then eval(args[0], env) + eval(args[1], env) when :* then eval(args[0], env) * eval(args[1], env) 本来の Lisp + も、ただの変数 環境から探索するため、実行時の差し替えも自由。 (define + -) のような再定義も可能。 Fast-path (最適化ショートカット) Ruby の + へ直行 環境探索をバイパス。実際に走るのは、 C実装で最適化済みのRuby本体の演算処理。 VAR判定 言語の意味論を壊す反則行為 のため → イエローカード
  11. S式は、もう ASTだ。 01 S式 (Lispコード) (+ 1 2) ▶ 02

    RubyのAST [:+, 1, 2] 自作Lispのパーサー 37行 S式をそのまま読むだけ CRubyのパーサー 数万行 複雑な文法と格闘 構文自体がAST → パーサーをサボれる → 心臓部のevalに注力できる 12 ハーフタイム| Column: なぜLispを自作?
  12. 14 後半 | 解析と実行の分離 構文判定は、 1回でいい。 Before:実行のたびに判定をやり直す # 実行のたびにASTを歩いて構文判定する def

    eval(exp, env) case exp ... # ← 描画1回で何千万回も通る! After:一度だけ解析し、クロージャに変換 # 事前に1度だけ解析し実行専用コードを得る def analyze(exp) case exp # ← 通るのは解析時の1回だけ when Symbol then ->(env) { env.get(exp) } 💡 意味論 = 「 + は環境から探す」などの、言語のルールブック Fast-path ルールを変えて速くする → 反則 解析と実行の分離 段取りを変えて速くする → フェア
  13. 高速化でも、まだ51倍差。 Before 最適化前 11.33 s After 最適化後 7.31 s 約

    1.55 倍速 Gauche 目標・実用言語 0.14 s 約 51 倍の差 timeコマンド生ログ ❯ time ruby bin/repl.rb examples/mandelbrot.lisp # 最適化前 (中略) ruby bin/repl.rb lisp 11.09s user 0.10s system 98% cpu 11.398 total ❯ time ruby bin/repl.rb examples/mandelbrot.lisp # 最適化後 (中略) ruby bin/repl.rb lisp 7.25s user 0.08s system 97% cpu 7.535 total ❯ time gosh examples/mandelbrot.lisp # Gauche (中略) gosh examples/mandelbrot.lisp 0.15s user 0.02s system 132% cpu 0.123 total 【公式記録】同一マシン・5回計測の中央値 残る圧倒的な速度差のヒントは、RubyKaigiで手に入れた 冒険の書にあった 16 アディショナルタイム | 結果発表
  14. 17 戦術分析 | 最適化の鍵は冒険書にあった 鍵は、冒険書にあった。 『CRuby quest 〜 Rubyのぼうけんのしょ 〜』

    著者: youchan / 弊社ANDPAD所属 01 本書のテーマ CRubyのソースコードをParser, AST, ISeq, VMのセクションごとに解 読し、Ruby内部の構造を深く理解する。 自作Lisp (7.31 s) — 2工程 01 Parser 37行のサボり ➔ 02 AST そのまま直接実行 CRuby / Gauche (0.14 s) — 4工程 01 Parser 本格的な構文解 析 ➔ 02 AST 抽象構文木の構 築 ➔ 03 ISeq バイトコードに 翻訳 ➔ 04 VM 仮想マシンで実 行 次はVMも自作したい! 🚀 ここが最適化さ れている!
  15. 01 言語の基本が理解できた 言語制作に必要なコア知識の習得 lexer(字句解析) 、parser(構文解析) 、 evaluator(評価器) といった、プログラミング 言語を一から構築する上で必須となる基礎知識を 体系的に理解できました。

    02 最適化とは地道な作業の積み重ね マインドセットの大きな変化 最適化とは、一握りの「強いエンジニア」だけが 魔法のように行う特別な技法ではありません。泥 臭く、地道な測定 とコード改善 の積み重ねこそが 本質であると深く理解できました。 18 戦術分析| 作ってみてどうだったか