夏休みの自由研究として自作プログラミング言語をClaudeでVibe Codingした話
前書き:妻と息子が不在の一足早い夏休み
仕事が谷間(= 夏休みが取れる状況)、かつ妻と息子が旅行で不在だったので、ある程度規模感があるOSSを作ろうと思いたちました。今回、実装期間2日で開発したのは、SNOW言語(関数型言語)です(補足:夏休みは2日以上あります)
今回、自作プログラミング言語の開発を自由研究の題材とした目的には、以下の3点があります。
- コンパイラ作成を通した関数型言語(OCaml)の学習
- 全実装をVibe Codingすることによる利点・欠点の把握
- プログラミング言語を作る上で考慮すべき事項の把握
これから取り留めのない話をズラズラと書き綴る予定なので、上記の目的を達成できたかを先に記載します。
- Claudeが全部書いてしまうので、関数型言語の理解度は低いまま。
- Vibe Codingの利点は圧倒的な実装スピード。2日で1.2万行を実装。欠点は「実装規模の拡大」と「仕様の曖昧さ」によって、Claudeのコード生成の質が低下。ユニットテストを通せるが、結合テストでエラーが多発。
- 「libcを使うかどうか」や「クロスプラットフォーム対応への考慮」など、実装フェーズで気づけるフィードバックが多数得られた。
SNOW言語とは
SNOW言語は、シンプルな構文を持つ関数型言語に、Golangに似たエコシステムを併せ持つつもりの自作プログラミング言語です。関数型言語の勉強のため、コンパイラをOCamlで実装しています。SNOW言語は現在、四則演算ができる程度の完成度であり、ネストが深いコードを書かれると恐らくコンパイルできないでしょう。
過去の記事「GoユーザーがHaskell/OCamlのライブラリ配布で面食らった話」で書いたように、私の知る範囲では関数型言語はライブラリ配布が面倒な側面があります。公式が提供するエコシステムが弱く、サードパーティ(OSS)がエコシステムの貧弱さを補っている印象がありました。
個人的には、依存関係の解決が容易だったり、簡単にバイナリやライブラリを配布できる言語が、学習しやすいと考えています。言い換えると、Golangのエコシステムが私好みであり、同等のエコシステムを持つ関数型言語を求めていました。そのような言語は無かったので(Golangで samber/lo 等のライブラリを使う発想はあったが)、夏休みが取れるタイミングで「無ければ作ろう」と思い立ちました。「Go、Rust、OCamlのいいとこ取りをした言語がいいな」と、漠然と考えながら、夏休み前に仕様を雑に書いてから、夏休みに入りました。
SNOW言語の名前の由来は、雪と共に暮らす新潟県民なので、何となく雪(SNOW)を採用しました。雪はキレイですけど、雪かき大変ですよね。良い面と悪い面の両方が名前から感じられ、清濁併せ呑んでいる単語なので採用しました。言語開発は大変だけど、学びになる筈だよな、という気持ちが込められています。
CopilotからClaudeに乗り換えた
私は、GitHub Copilotが登場した頃から、Copilotを使い続けていました。しかし、今回はClaudeを選択しました。
乗り換えをした理由は、以下の2つです。
- 会社でGitHub CopilotとClaudeがPull Requestレビューを実施しており、Claudeの方がアウトプットの質が良かった。
- GitHub Copilotに丁寧な指示を出しても、コードを書ききれないケースがあった。
気持ちとしては「お試ししてみたかった」ぐらいのレベルです。会社でClaudeを利用できましたが、GitHub Copilotで十分な開発速度を出せていたので、業務では乗り換えるモチベが低かったです。しかし、今回は家族が居ない2日で勝負を決めたかったので、性能の良さそうなClaudeを試すモチベがありました。
ちなみに、丸一日コーディングしていると、Proプラン(月額20ドル)では全然足りず、Maxプラン(月額100ドル)でギリギリでした。Maxプランでも、rate limit に引っかかる時がありました。夏休みが終われば、趣味のOSS開発に時間を取れないのでProプランで十分な印象です。
Claudeの性能に驚いたが、後半は作文時間が伸びた
Claudeに型システムを実装させる段階では、その圧倒的な性能に驚きました。実装速度が早いだけでなく、エラー発生時の問題解決能力が高かったです。GitHub Copilotは一度躓くと正しい実装に導くのに一定の労力がかかりますが、Claudeは自己解決してくれます。この事実に気づいたあたりで、私はYouTubeで大空スバルのドンキーコング実況を見始めました。
Claudeは数時間でLexerやParserの実装を完了させ、1日でLLVMと統合し、簡単なソースコードをビルドできるようにしました。私は、今回の夏休み期間(約1週間)を全て利用してもLLVMと統合できないと考えていたので、圧倒的な実装速度にご満悦でした。
しかし、Goのエコシステムを参考にした仕様を実装するあたりで、Claudeのコード生成が怪しくなってきました。このタイミングでは、コード規模が8,000行程度であり、仕様が曖昧になっていました。私はGolangユーザーなので、「go mod tidy
で依存パッケージを管理するアレを作りたい」程度のことを考えただけで、正確な内部仕様をClaudeに渡しませんでした。
その結果、パッケージ管理システムが中々完成しませんでした。特定のディレクトリにいると依存パッケージ解決とビルドが成功するが、別リポジトリの少し複雑なディレクトリ構成のコードをビルドできない状態に陥ったりしました。例えば、ビルド時にリモート(フェッチ対象)とローカルのソースコードのどちらを参照するかの仕様が明確に言語化されていなかったので、Claudeのコード生成が曖昧な状態になっていました。
Claudeのコードをレビューできなくなった
私は、殆どOCamlでコードを書いたことがありません。コードを書く時に手が止まるレベルです。そんな私がClaudeの生成するOCamlコードを正確にレビューできるわけがありません。次第に、実行結果(ユニットテスト)で判断するようになってきました。このような状態なので、全くOCamlの勉強にならず、レビューも放棄していました。
今回のVibe Codingを通して、LLMと共にプログラミング言語を学ぶ難しさを感じました。知識がない状態だと、Claudeが生成するコードは正しいように見えます。しかも、圧倒的な速度で次から次へとコードを生成してくるので、途中で「動いているからマージするか」と判断してしまいます。気合を入れてレビューできる量には限界があるので、ある一定のコード量を超えるとレビューが雑になります。そして、読むだけでは全く頭に入りません。
大事なのは、正確な仕様、統合テスト、言語スキル
急ピッチでSNOW言語の開発を進め、現在はメンテナンス困難な状態に陥りました。開発が難しくなったので、本記事を書き始めたという訳です。
今までも理解していましたが、Claude(や他のLLM)とVibe Codingする上で重要なのは、やはり詳細設計でした。日本語の作文能力が求められます。今回の開発では、それなりの仕様書がリポジトリに存在する時は良いコードが生成されましたが、プロンプトで数行レベルの仕様(要件)を伝える方法に切り替えたあたりから、雲行きが怪しくなりました。やがて、小規模の変更で壊れるソフトが出来上がりました。
仕様が曖昧だと、ツギハギだらけの脆いコードが出来上がります。その場その場を乗り切るためのコードをClaudeが生成し続け、やがて破綻します。2日間で、破綻するところまで開発を進める経験ができたのが、今回の収穫です。私は今回の開発を通して、新卒研修で実装したソフトを思い出しました。頑張って書いたのに、一箇所直すと壊れてしまうあの感じ。
メンテナンス不能な状態に陥ることを回避するには、ユニットテストだけでなく、結合テストを書くことが重要だと悟りました。LexerやParserのユニットテストがパスしていても、CLI(コンパイラコマンド)としてのテストが不足していると、何故かビルドが通らなかったり依存関係の解決が壊れたりします。面倒臭がって、仕様が曖昧な状態でClaudeにコード生成を依頼し始めると、良質な結合テストが漏れるのだなと体感しました。
また、プログラミング言語スキルの重要性も痛感しました。自分が習得していない言語のコードをLLMに生成させるのは、アンチパターンと言えるでしょう。ゴミを量産しているだけです。マージして良い品質かどうかを見極めるスキルが必要です。そのうち、プレイングマネージャーや他職種の人が「LLMでコードを書いておきました(動くが、出来が悪いコードをポイッ)」と言い始めて、レビュアーから怒りを買う話がSNSに流れてきそうだなと予想しています。
プログラミング言語開発で低レイヤーを考える必要性
今回、SNOW言語を実装して初めて気づいたのですが、プログラミング言語開発でも低レイヤーの処理が登場します。Println()
を実装するタイミングで「あっ、OSに依存する処理だ(そりゃそうだ)」と、仕様の考慮漏れに気づきました。実装前は、パースのことしか頭にありませんでした。デバッグ用にLLVMの機能で printf()
を呼び出せますが、その対応をいつまでも続ける訳には行きません。
libc の関数をFFI(Foreign Function Interface)して呼び出すと、libc に依存してしまいます。かと言って、自前でアセンブラを書いて直接システムコールを叩こうとすると、OSやアーキテクチャによって対応がバラバラです。今回は、FFIを採用しましたが、この辺りは再設計する想定です。
最後に:SNOW言語コンパイラをGolangで書き直す
今回は、不慣れなOCamlを使い、かつ仕様も曖昧なままでSNOW言語を開発したので、メンテナンスできない状態となってしまいました。今回の課題を踏まえて、次は仕様を明確にした上で、私の得意な言語(Golang)でコンパイラを再実装するつもりです(記事中では触れていませんが、CLAUDE.md
が雑だったので、こちらも改良予定です)。
関数型言語を学習する目的は、SNOW言語の標準ライブラリを開発する最中に学べるはずなので、しばらくはこの方向性で進めていきます。
ロシア人と国際結婚した地方エンジニア。
小学〜大学院、就職の全てが新潟。
大学の専攻は福祉工学だったのに、エンジニアとして就職。新卒入社した会社ではOS開発や半導体露光装置ソフトを開発。現在はサーバーサイドエンジニアとして修行中。HR/HM(メタル)とロシア妻が好き。サイトに関するお問い合わせやTwitterフォローは、お気軽にどうぞ。