Ruby の構文で Python ライクに関数を操る
- 再帰下降 & Pratt パーサでつくる自作言語 -

2025年12月18日掲載

キービジュアル

この記事は、ソフトバンクアドベントカレンダー2025の18日目の記事です。

Ruby 本体(CRuby)に新たなパーサである Prism が導入されました。これまでの Yacc / Bison などのパーサジェネレータに依存した実装から、より可読性が高く構造化された手書きパーサへの移行は、言語処理系の実装において非常に興味深い潮流です。

この流れに触れ、「自分も一度、パーサジェネレータ頼らず、自分の手でパーサから実装した言語を作りたい」と思い立ちました。

そこで今回は、パーサからすべてスクラッチで実装しつつ、

  • Ruby の洗練された構文
  • Python / JavaScript のような First-class functions(第一級関数)

を融合させた、独自の言語を自作してみました。

本記事では、その設計思想と、どのような開発過程をとったのかについて紹介します。

目次

コンセプト:Ruby の「書き味」× Python の「関数モデル」

パーサから自作する、という方針を決定後、次に設計の焦点となったのは「どのようなセマンティクス(言語仕様)にするか」という問いでした。

Ruby の構文は非常に洗練されています。Ruby の哲学は徹底して「すべてがオブジェクト」であり、メソッド呼び出しは、あるオブジェクトに紐づく振る舞いとして解決される設計です。Proc や Lambda はありますが、Python や JavaScript のように「関数をシンプルに値として扱う」文化とは異なります。

そこで本プロジェクトでは、あえて Ruby という完成された体系に異質な哲学を持ち込み、「Ruby の文法的な心地よさを維持しつつ、関数を『値』として手軽に持ち運べる世界」を目指し、そこを差別化ポイントとしました。

言語仕様のハイライト:Ruby 構文と関数モデルの融合

本言語の設計における最大の焦点は、Ruby 特有の記述性を損なうことなく、関数をより柔軟に扱う点にあります。

具体的なコード例を通じて、本言語が持つ「Ruby のようでいて、異なる挙動」を示す主要な特徴を紹介します。

  • 関数およびメソッドの第一級オブジェクト化(& 演算子による参照)

一言で述べるとこうでしょう。オブジェクト指向的な記述と関数型的な取り回しが共存する、独自のコーディング体験です。

1. グローバル関数の第一級オブジェクト化

Ruby においてメソッドを値として扱う場合、通常は method(:name) のような変換プロセスを経る必要がありますが、本言語では & 演算子を用いることで、関数をダイレクトに値として取得できます。

def multiply(a, b)
  return a * b
end
bound_mul = &multiply
print bound_mul(6, 9)  # => 54

構文上のノイズを最小限に抑え、関数を「値」として自然に扱うための設計です。

2.レシーバを束縛したインスタンスメソッドの抽出

インスタンスメソッドについても同様のアプローチをとります。特定のインスタンス(レシーバ)とメソッドを紐付けた状態で、関数オブジェクトとして抽出できます。

user_bob = User.new("Bob")
bound_getter = &user_bob.get_name
print bound_getter()  # => Bob

3.ブロック構文の採用と関数モデルへの統合

Ruby のブロック(do ... end)は完全に踏襲しています。しかし、内部的には特殊なブロック引数ではなく、「単なる関数オブジェクト」として処理される点が異なります。

def repeat3(block)
  block(1)
  block(2)
  block(3)
end
repeat3 do |n|
  print n * 2
end

これにより、書き味は馴染み深い Ruby のままでありながら、内部モデルはシンプルな関数呼び出しとして一貫性が保たれています。

4.メソッドチェーンのコンテキスト保持とバインディング

本言語の表現力が特に際立つのが、メソッドチェーンとバインディングの組み合わせです。一連のメソッドチェーンを実行し、その構築された状態(コンテキスト)を持つ最終メソッドを関数として切り出すことができます。

greeting = &"Hello"
  .append(" world")
  .append("!")
  .upcase
print greeting # => "HELLO WORLD!"

5.Staticメソッドのバインディング

Static メソッド(クラスメソッド)に関しても、一貫して & 演算子で扱えます。

bound_static_log = &Logger.log
bound_static_log("Bound static call")

実装基盤の選定:軽量なスタック VM clox の採用

どのように作り上げていったのか、そのアプローチについて紹介していきます。

言語処理系の実装にあたり、ベースとなるアーキテクチャとして Robert Nystrom 氏の名著『Crafting Interpreters』で体系的に解説されている clox を採用しました。

clox が、再帰下降 + Pratt を利用していた、というのが一番の理由ですが、他にも採用したわけがあります。

clox は C 言語で書かれた約 2,500 行というコンパクトな実装でありながら、現代的な動的言語に必要なコア機能を網羅しています。

  • スタックベース仮想マシン(VM)
  • バイトコードコンパイラ
  • 関数とクロージャ
  • クラスと継承
  • ガベージコレクション

これらはまさに「本物の言語」を構成する必要最小限かつ十分なスタートとなる要素です。

1.CRuby ではなく、なぜスクラッチベースなのか

「Ruby のような構文」を目指すのであれば、CRuby(Ruby 本体)のソースコードをフォークして改造するという選択肢も考えられます。しかし、歴史の長い CRuby は巨大かつ高度に最適化されており、私の技術レベルで、実験的に「言語の思想(セマンティクス)」を書き換えるための土台としては複雑すぎました。

対して clox の最大の利点は、「全体を完全に読み切り、把握できるサイズ感」にあります。

  • 可読性: 作者の意図を隅々まで追える
  • 可塑性: 変更と挙動の因果関係が明確
  • 回復性: 破壊的な変更を行っても修正が容易

巨大な実用言語の挙動に頼るのではなく、構造が明快な「箱庭」を用意することで、言語仕様の変更が内部構造にどう影響するかをダイレクトに体感しながら開発を進めることができました。

構文解析戦略:再帰下降と Pratt パーサのハイブリッド構成

再帰下降と Pratt についても説明を加えておきましょう。

メンテナンス性と拡張性を重視し、ブラックボックス化しやすいパーサジェネレータは使用せず、以下のハイブリッド構成を採用しました。

  • 文(Statement): 再帰下降パーサ(Recursive Descent Parsing)
  • 式(Expression): Pratt パーサ(Top-Down Operator Precedence Parsing)

1.再帰下降パーサ:構造化された文法への適合

if ... end や class ... end、そして do ... end といった Ruby 特有のブロック構造を持つ構文は、開始と終了が明確なため、再帰下降パーサとの相性が極めて良好です。

コードの流れをそのまま関数の呼び出し階層として表現できるため、文の境界(Statement Boundary)が明確になり、パーサ自体の可読性も向上します。

2.Pratt パーサ:柔軟な式の拡張

一方で、式(Expression)の解析には、演算子の優先順位と結合規則を柔軟に定義できる Pratt パーサを採用しました。本言語で導入した以下の機能は、単純な再帰下降では記述が複雑になりがちですが、Pratt パーサであれば「優先順位(Precedence)」と「前置 / 中置 / 後置(Prefix / Infix / Postfix)」の定義だけでシンプルに実装可能です。

  • メソッドチェーン
  • 中置演算子
  • &foo のような前置バインド構文
  • 後置ブロック
  • 優先順位によるメソッドチェーンの解決

特に、ドット(.)演算子に最高の優先順位(Highest Precedence)を割り当てることで、a.b.c().d のような長いメソッドチェーンも、特別な文法規則を追加することなく自然に解析されます。

「Ruby らしさ」を維持しつつ、独自の演算子やバインディング構文をシームレスに統合できたのは、この Pratt パーサの柔軟性があってこそです。

開発手法:AI 時代における「スモールスタート」言語設計

AI による支援が利用可能な現代においては、clox をベースとして、AI と対話的に機能を拡張していくアジャイルなアプローチをとりました。

1.段階的実装

わずか 2,500 行という clox のコンパクトさは大きな武器となりました。巨大なコードベースと格闘することなく、以下のような段階的な機能追加をスムーズに行うことができました。

例えば、

  1. まず基本となる def(関数定義)を実装する
  2. 次に do ... end(ブロック構文)を追加する
  3. さらに実験的な &func(バインド構文)を試行する

このように、ベースとなる処理系が軽量であるからこそ、アイデアを即座にコードに落とし込み、動作を確認しながら次のステップへ進むという「高速なイテレーション」が可能になります。

2.AI は「コード生成機」ではなく「仕様の壁打ち相手」

AI 活用で特筆すべきは、AI を単にコードを書かせるためのツールとしてではなく、「仕様検討のパートナー」としても位置づけた点です。

  • 「Ruby 風の構文にする場合、どこまで仕様を寄せるべきか?」
  • 「バインド時の挙動はどうあるべきか?」

こうした設計判断に迷うポイントについて、AI と議論(ディスカッション)を重ねることで、メリット・デメリットを整理し、仕様決定のプロセスを劇的に加速させることができました。

AI を「思考の拡張」として活用し、アイデアの検証サイクルを回すこのスタイルこそ、現代における新しい言語開発の形と言えるかもしれません。

Next Step: JIT 実装とクラウドインフラの活用

現在、次のフェーズとして、バイトコード実行から機械語を動的に生成する JIT(Just-In-Time)コンパイラ の実装に着手しています。しかし、ここで新たな壁に直面しました。「ハードウェアの壁」です。

JIT コンパイラを正しく実装するには、x86_64 だけでなく、ARM アーキテクチャ や Windows 環境など、多様なプラットフォームでの動作検証が不可欠です。しかし、個人でこれら物理マシンをすべて調達し、管理するのは現実的ではありません。

ここで活きたのが、普段の業務で培った「クラウドエンジニア」としての視点でした。

  • 多種多様なインスタンス: ARM ベースのプロセッサや Windows Server など、必要な環境を必要な時だけ数秒で立ち上げる。
  • CI/CD パイプライン: コードを Push した瞬間に、クラウド上の異なるアーキテクチャで並列テストを回す。

「言語処理系の自作」は低レイヤーの探求ですが、その開発効率を最大化するのは現代的な「クラウドインフラ」です。

このように、泥臭い低レイヤー技術と、スケーラブルなクラウド技術を組み合わせられる点も、現代のエンジニアが言語開発を楽しむ醍醐味の一つだと感じています。

まとめ:言語実装という「手の届く実験場」

今回ご紹介したのは、Ruby の書き味と Python / JS の関数モデルを融合させた、小さな実験的なプログラミング言語です。

その構成要素を振り返ると、決して魔法のような技術を使ったわけではありません。

  • clox という「読み切れる実装」を土台にする
  • 再帰下降と Pratt パーサで着実に文法を組む
  • AI を壁打ち相手にして、試行錯誤を高速化する

これらはすべて、現代のエンジニアであれば誰でもアクセス可能なアプローチです。

言語を作るために、数百ページの巨大な仕様書や、最初から 100% の完成度は必要ありません。まずは動く最小限の言語を作り、そこに自分なりの「世界観」や「欲しい機能」を少しずつ実装していけば良いのです。

自分だけの理想の言語を乗せて動かす体験。それは、普段使っているツールの裏側にある景色を一変させる、忘れられないエンジニアリング体験になるはずです。

アドベントカレンダー、19日目もおたのしみに!

関連サービス

ソフトバンクはAWS アドバンストティアサービスパートナーです。「はじめてのAWS導入」から大規模なサービス基盤や基幹システムの構築まで、お客さまのご要望にあわせて最適なAWS環境の導入を支援します。

Microsoft Azureは、Microsoftが提供するパブリッククラウドプラットフォームです。コンピューティングからデータ保存、アプリケーションなどのリソースを、必要な時に必要な量だけ従量課金で利用することができます。

Google サービスを支える、信頼性に富んだクラウドサービスです。お客さまのニーズにあわせて利用可能なコンピューティングサービスに始まり、データから価値を導き出す情報分析や、最先端の機械学習技術が搭載されています。

MSP(Managed Service Provider)サービスは、お客さまのパブリッククラウドの導入から運用までをトータルでご提供するマネージドサービスです。

おすすめの記事

条件に該当するページがございません