🛤️

ポエム:LLM時代のライブラリ設計、LLMが書きやすいものにした方が良いので泣く泣く方針転換した

に公開2

本当は、今でもRailway Oriented Programmingで書きたい

株式会社ジェイテックジャパン CTOの高丘 @tomohisaです。

私は Railway Oriented Programming が好きで、C#で実現するために ResultBox というライブラリを作り、自社のイベントソーシングライブラリ Sekiban にも組み込んできました。自分がメインで作る小さなプロジェクトでは問題なく機能していましたが、チーム開発とLLM時代の到来により、方針転換を決断しました。

この記事は、自分の好みよりチームとLLMとの協働性を優先した、ライブラリ開発者の決断の記録です。

Railway Oriented Programmingの魅力

Railway Oriented Programming(ROP)は、Scott Wlaschin氏が提唱したエラーハンドリングのパターンです。F#などの関数型言語で一般的な Result<T, E> 型を使い、成功と失敗の2つのレールを走る列車のように、処理を連鎖させていく手法です。

例外を投げるのではなく、エラーを値として扱うため、エラーハンドリングが明示的になり、型システムがエラーケースを強制してくれます。関数の合成も容易で、テストもしやすい。このアプローチが気に入って、C#でも実現したいと思い、ResultBoxライブラリを開発しました。

小規模プロジェクトでの成功体験

ResultBoxを使った開発は、私がメインで設計・実装する小さなプロジェクトでは理想的でした。

細かな関数を定義する習慣ができて、各関数が単一の責任を持ち、合成可能になります。データフローが明確で、エラーハンドリングが統一的で読みやすい。例外を投げないことで、パフォーマンスも一部向上します。

専門家である自分が中心となって書くコードなら、このスタイルで問題ありませんでした。

直面した2つの課題

しかし、プロジェクトの規模が大きくなるにつれて、2つの大きな壁に直面することになりました。

1. チーム開発における「パラダイムのギャップ」

チーム開発の段階になると、メンバー間の実装スタイルの違いが顕在化しました。

メンバーは、ResultBoxの書き方を真似ようとしつつも、慣れたthrowと混ざって書いてしまいます。これは決して悪いことではありません。それがC#の「普通」の書き方であり、新しいパラダイムへの移行は容易ではないからです。

Railway Oriented Programmingは、単なるライブラリの使い方ではなく、プログラミングパラダイムそのものの変更を要求します。メンバーにとっては:

  • 新しい概念の理解が必要
  • 慣れ親しんだtry-catchと全く違うアプローチ
  • サンプルコードを見ても、すぐには実践できない

私はサンプルプログラムを共有したり、Agent.mdに記載したり、MCP(Model Context Protocol)でドキュメントを渡したりと、様々な方法を試しました。

しかし、習得には時間がかかります。この頃から「チームには学習コストが高すぎるのではないか」と考え始めました。

2. LLMに「書き方」を教えるコスト

さらに大きな課題となったのが、LLMとの相性です。

LLMは、言語の基本機能や有名なライブラリの使い方をよく理解しています。モデル内で学習済みのため、コンテキストを与えなくても適切に使用してくれます。

しかし、Railway Oriented Programmingは異なります。

通常のパラダイムと異なる実践方式を使うときには:

  • インストラクションでコンテキストを明示的に渡す必要がある
  • サンプルコードやパターンを何度も示す必要がある
  • プロンプトが長くなり、本質的な仕様の説明が圧迫される

現在、私たちは「いかに仕様をクリアにLLMに伝えるか」で日々試行錯誤しています。ビジネスロジックの説明、ドメインの知識、複雑な要件など、伝えるべきことは山ほどあります。

そんな中で、「コーディング手法」のために追加プロンプトを書くことが、大きなコストに感じられるようになりました。本当に伝えたいのは仕様であって、書き方ではないのです。

方針転換の決め手

悩んでいた私にとって、決定打となった気づきが2つありました。

言語の「木目」に沿うということ

一つ目は、Scott Wlaschin氏のポッドキャストを翻訳した時の言葉でした(翻訳記事はこちら)。

彼が語った言葉です:

木工では、木には木目があります。木目に沿って割ると簡単ですが、木目に逆らうと難しいです。木目に沿って行くべきです。関数型言語があれば、関数型的に考えることを学ぶ必要があります。

この言葉で、状況が明確になりました。

C#の「木目」は、例外処理です。それが言語の設計であり、エコシステムであり、開発者の常識です。F#のように関数型で設計された言語とは、根本から異なります。

私がやっていたのは、C#という木の木目に逆らって割ろうとする行為だったのです。一人なら力技でなんとかなりますが、チームで、LLMと協働するとなると、話は別でした。

パラダイムシフトは「一度にひとつ」が原則

もう一つは、チームの認知負荷についての気づきです。

冷静に考えると、私たちのチーム、すでにイベントソーシングという大きなパラダイムシフトを導入しています。これだけでも学習コストが高い。

そこにさらに:

  • イベントソーシングを学んでもらって
  • 関数型C#も学んでもらう

これはF#を新しく学んでもらうのと同じくらい難しいことだと気づきました。

何を守って、何を諦めるか

ここで重要なのは、パラダイムシフトそのものを諦めたわけではないということです。

Sekibanで大事にしているのは:

  • ビジネスロジックをPure C#で実現すること
  • シンプルなレコード型のデータと、それを変換する関数で構成すること
  • ドメインの質に直接貢献するパラダイムシフト

これらの本質的な部分は、必ず守る必要がありました。

一方で諦めたのは、「書き方の作法」です。関数型的な書き方、Railway Oriented Programmingという表面的なスタイル。

通常のC#の文法(try-catch、例外処理)を使いながらも、Pure functionとイミュータブルなデータ構造という関数型の本質は保つ。これが私の選択でした。

2つのライブラリへの分離

この方針転換を実現するため、Sekibanの最新バージョンであるDcb版に大幅な改良を加えました。

WithResult と WithoutResult

Sekibanの最新バージョン 10.0 では:

  • Sekiban.Dcb.WithResult:Result型を使うプロジェクト向け
  • Sekiban.Dcb.WithoutResult:例外を使うプロジェクト向け

この2つを別のNuGetパッケージとして分離しました。

この設計の利点

この設計には、いくつかの利点があります:

  1. 選択を強制:パッケージを導入する時点で、開発者はどちらかを選択し、選択後はそれしか使えない
  2. 名前の統一:クラス名、namespace、メソッド名をWithResultとWithoutResultで同じにできる
  3. 命名の妥協が不要:どちらかが「良い名前」で、もう一方が「WithResultSuffix付きの長い名前」にならなくて済む

社内プロジェクトの移行

現在、社内プロジェクトはすべてWithoutResultに書き換えました。

正直なところ、LLMを使っておらず、一人だけで開発していたら、今でもWithResultを使いたいという気持ちはあります。

しかし、チーム開発とLLM活用の現実を考えると、WithoutResultが正しい選択でした。

ライブラリ開発者として学んだこと

この経験から、ライブラリ開発者として、いくつかの重要なことを学びました。

1. 技術選択は理想だけでは決められない

個人的に美しいと思う技術が、必ずしもチームに最適とは限りません。

2. 言語の「木目」を尊重する

言語が設計された通りに使うことで、エコシステム全体の恩恵を受けられます。

3. LLM時代のコンテキスト管理

LLMに教えるべきは「仕様」であり、「コーディング手法」にプロンプトを割くのはコストが高すぎます。

4. パラダイムシフトの優先順位

すべてのパラダイムシフトを同時に導入するのではなく、ビジネス価値に直結するものを優先すべきです。

5. ライブラリ設計の柔軟性

WithResultとWithoutResultを別パッケージにすることで、両方のユーザーを満足させることができました。

LLM時代のコード記述とフレームワーク設計

この経験を通じて、LLM時代におけるコード記述の考え方も変わってきました。

コード量よりも明確さ

従来は「簡潔なコード」が良いとされてきましたが、LLM時代においては**「LLMが正確に書きやすいコード」**の方が重要だと考えています。

例えば、冗長な変換メソッドであっても、LLMにとってはルールが明確であれば難しくありません。むしろ、暗黙的な変換や複雑な抽象化よりも、明示的で冗長なコードの方が、LLMは正確に生成できます。

今後は、LLMに特化した記述ができるようにフレームワークも改善していく必要があると考えています。

言語選択もLLM視点で

言語の選択についても、同様の視点が重要です。

個人的には、型がない言語や、TypeScriptのように様々な手法が混在している言語よりも、GoやC#のようなシンプルな書き方ができる言語の方がLLMに向いていると感じています。

これらの言語の特徴:

  • 書き方が統一されている
  • 型システムが明確
  • 冗長でも読みやすい
  • LLMが学習しやすいパターン

冗長であっても、ルールが明確で一貫性のあるコードの方が、LLMは正確に生成・理解できるのです。

今後のSekiban

最新バージョン 10.0 は現在プレビュー段階です。

WithResultとWithoutResultの両方を提供することで:

  • 関数型プログラミングを実践したい開発者
  • チーム開発やLLM活用を優先する開発者

どちらのニーズにも応えられるライブラリを目指しています。

まとめ

Railway Oriented Programmingは、今でも美しく、強力な手法です。しかし、チーム開発とLLM時代においては、実用性とのバランスが重要です。

C#という言語の「木目」に逆らわず、しかしイベントソーシングやPure functionといった本質的な価値は守る。この現実的な選択が、長期的にチームとプロダクトにとって最善だと信じています。

理想を追求することも大切ですが、現実的な制約の中で最適解を見つけることもまた、エンジニアリングの重要な側面なのだと、この経験から学びました。


関連リンク:

ジェイテックジャパンブログ

Discussion

平鍋平鍋

とても共感できました。言語の木目とチーム開発、という観点が、LLMの登場がきっかけで炙り出されたという帰結が興味深かったです。

plusone|開発技法ノートplusone|開発技法ノート

木目の話を読んで、両刃のこぎりを思い浮かべました。
木目に沿って引く刃と、逆らって引く刃があるやつです。
使う刃を間違えると、うまく切れない。
結局「道具は適切に使え」ということですよね。

LLMを使った開発は、目的の筆頭に「LLMを使う/活かす」が来やすい分、
その前提で道具(言語や設計、運用の作法)を選び直す必要がある、というのが腹落ちしました。
LLMの特性に沿った設計に寄せたほうが、結果としてうまく回るんだろうなと思いました。