コードとは何か? 高いレベルで見れば、この 質問への答えは明らかであるように思える。コードとは、開発者が書くものである。つまり、 機械に何をすべきかを指示する、プログラミング言語で表現された命令である。 長年、コードを書くとは、1語1語をタイプすることだった。 進捗は、コードがどれだけ効率的に 作成、コンパイル、テスト、デプロイできるかによって測られる。 現代のLLMでは、コードを生成するためにすべての単語をタイプする必要はなくなった。 高いレベルの記述から、大量の実行可能なコードを生成できるようになった。 これにより、より深い問いが生まれる。コードの生成が 安価になった場合、コードの価値として残るものは何だろうか?

コードの2つの側面

コードは、常に2つの異なるが絡み合った目的を果たしてきた。

第一に、コードは機械への命令である。これは計算を指示し、 データを移動させ、ストレージとやり取りし、実行を調整する。LLMの時代において、 この部分はコモディティ化されつつある。

第二に、コードは問題領域の概念モデルである。これは「設計」の側面である。 よく設計されたコードベースは、機械への命令を含むだけでなく、 人間やツールが推論するための概念も含んでいる。

私たちがコーディングと呼ぶ活動は、これら2つの側面が交わる場所にある。 私たちは、システムの理解に使われる概念、名前、境界、関係性を形作っている。

概念モデルと語彙

概念モデルを明示的にすることは、コーディングのより深い側面であり、 ドメインとシステムが対処すべきユースケースによって駆動される。 すべてのドメインには、確立されたプロセス、プラクティス、そして何よりも 共有された語彙が存在する。 その語彙こそが、概念モデルを可視化するものである。

語彙とは通常、特定の言語や主題で使われる単語の集合として理解される。私がこの記事を書けるのは、 私が英語の語彙を知っているからである。読者がそれを読めるのは、 読者が私とその語彙を共有しているからである。

しかし、この記事を理解するためには、英語を知っているだけでは十分ではない。 この記事はソフトウェア開発に関するものである。ソフトウェア開発は、 独自の技術的語彙を持つ、広範で成熟した分野である。私が「抽象化」のような 言葉を使うとき、それは単なる英語の単語を使っているのではない。私は、 ソフトウェア開発特有の概念——独自の意味、歴史、含意を持つもの——を指している。 ソフトウェア開発に馴染みのない読者は、その言葉を表面的には理解できても、 この文脈でのより深い意味を見逃すかもしれない。独自の確立された語彙を持つ成熟した領域は、 ドメインと呼ばれる。

これはすべての真剣なドメインに当てはまる。コミュニケーションは共有された語彙に依存する。 相手が人間であれ、フレームワークであれ、LLMであれ、私たちが使う言葉は、 受け手が理解し、行動できる概念にマッピングされなければならない。

コードにおける語彙

よく設計されたコードベースは、ある種の語彙の表現である。 コードにおけるこの語彙はどこから来るのだろうか? ここにソフトウェア開発のユニークな性質が真に輝いている。 ソフトウェア開発は、さまざまなドメインの交差点で機能する。 一方には銀行、金融、小売、在庫管理、 ヘルスケア、保険などのドメインがあり、もう一方にはWeb、 インフラストラクチャ、AI、データエンジニアリングなどのドメインがある。

Web開発を行う人は、Webアーキテクチャ、Webメソッドのセマンティクス、GETの普遍的なキャッシュ 可能性、そしてそれらのセマンティクスの含意を強く把握する必要がある。 これを知らない人は、複雑なシステムをうまく設計できない。他のドメインでも同じことが言える。 語彙は単なるラベルの集まりではない。 それは意味、制約、設計上の結果を伴う。

小売ドメインを考えてみよう。小売向けのコードを書くとき、私たちは 顧客、製品、注文、出荷、支払いなどについて話す。小売ドメイン向けのWeb 開発を行う場合、コードには小売ドメインをWebドメインにマッピングする概念が含まれる。 例: Catalogはリソースであり、GET、POST、PUT、DELETEなどのHTTPメソッドを使って その上で操作を実行できる。コードを書く人は、両方の語彙に精通している必要がある。

ドメイン向けのコーディングは、根本的に翻訳の行為である。開発者は ドメインの語彙を技術ドメインの語彙にマッピングする。 その過程で、プログラミング言語が提供する構成要素を使って新しい語彙も構築される。 ログ、リポジトリ、クォーラム、トランザクション、moneyのような特定の概念などがある。 概念は型になり、関係はインターフェースになり、ルールは不変条件になり、 ワークフローはコンポジションになる。

変数の正確な名前、メソッドの境界、クラスの階層は、 段階的に発見されていく。適切な抽象化は、 最初から明らかであることは稀で、現実世界の制約に対してコードを継続的に成形し、 リファクタリングしていく中で初めて明らかになる。うまく使われたとき、 コードは徐々に、ドメイン自体の読みやすく高度に特化した表現になっていく。

技術ドメインでは、通常、ベースとなる実装パターンを提供するフレームワークやライブラリが見つかる。 フレームワークとライブラリは、語彙を体系化したものである。 それらは最も一般的な使用パターンを捉えている。そのため、Webやインテグレーション、 関連する関心事を含むエンタープライズアプリケーションを構築するための Spring Frameworkのようなエコシステムが存在する。 異なるプログラミング言語は、それぞれ独自の風味を持ち、 そのフレームワークやライブラリに反映・体系化される特定の設計制約を伴う。

境界づけられたコンテキストとローカルな語彙

フレームワークは、ドメインが安定した繰り返し可能な構造と広く共有されたセマンティクスを持つ場合にうまく機能する。 しかし、「オンライン小売」や「証券取引所」のようなものは異なる。 それらは単なる技術スタックではない。 普遍的な高レベルフレームワークが存在しない主な理由は、ドメインのすべてのインスタンス間で 語彙が十分に安定していないためである。 普遍的な抽象化を見つけようとする試みは、役に立つにはあまりに汎用的になり、 広く適用可能にするにはあまりに独善的になる。 コアビジネスモデルに近づくほど、 抽象化はローカルに発見されなければならない。 これが、ドメイン駆動設計における境界づけられたコンテキストの考え方が重要である理由である。 境界づけられたコンテキストは、特定の語彙とモデルが有効である境界を示す。 同じ単語が異なるコンテキストで異なる意味を持つことがあり、 各コンテキストは独自の抽象化、ルール、言語を必要とする。

これらのローカルな抽象化と語彙をどのように構築するのか? この語彙の多くは、コードを書いて振り返る反復的なセッションを通じて構築される。 TDDのような手法は、この反復的な語彙の開発に優れている。 それらは、モデルとその振る舞いの間の継続的なフィードバックを強制することで、 正しい名前、正しい抽象化、正しい境界を発見するのに役立つ。

コーディングは孤立して行うことはできない。ドメインエキスパート、ユーザー、開発者の間で 緊密なコラボレーションが必要である。 このコラボレーションは、これらのローカルな抽象化と語彙を構築するために必要である。

これはアジャイルソフトウェア開発の教訓に直接つながる。個人と対話、顧客とのコラボレーション、 動くソフトウェア、変化への対応を重視することは、単なるプロセスの助言ではない。 それは、フィードバックを通じて語彙を発見し、洗練させる方法である。

ドメイン駆動設計は、ユビキタス言語の考え方を通じてこれをより明確にしている。 ユビキタス言語とは、開発者とドメインエキスパートによって開発され、動くソフトウェアに対して継続的にテストされる共有言語である。

思考ツールとしてのプログラミング言語

コードを通じて語彙を構築するには、コードを書いて再形成する積極的な関与が必要である。 生成されたコードをただ受動的にレビューするだけでは不十分である。コードについて深く考える行為は、 しばしば私たちが積極的にコードを書いているときにのみ起こる。 プログラミング言語とその構成要素および制約は、 それ自体が思考ツールになる。異なるプログラミング言語が提供する設計制約は、 私たちの思考を形作るのに役立つ。Goのチャネルと軽量スレッド、Javaのオブジェクト指向モデル、Rustの所有権モデルは、 いずれも私たちに構造、境界、トレードオフを特定の方法で見ることを促す。 その意味で、プログラミング言語は設計を表現するだけでなく、 設計を発見するのにも役立つ。私は最近、非同期プログラミングの例のために カスタムのFuture実装を設計する必要があった。 Future APIの重要な側面の1つは、一連のアクションを表現できるように コンポジションを設計することである。

         var future1 = action1();
         future1.thenCompose(val1 -> action2(val1))
                .thenCompose(val2 -> action3(val2))
        

このAPIをうまく実装するには、関数型プログラミングの概念と語彙を知ることが不可欠である。 これらの概念を知らないと、実装や使用がぎこちないものになる。

時として、プログラミング言語の構文があまりに冗長になり、 解決策の根本的な構造を隠してしまうことがある。 例えば、最近ワークショップのためにスナップショット分離の実装を扱っていたとき、 本質的な要件を平易な英語で記述するのはやや曖昧で、Javaコードに落とし込むのはあまりに冗長だった。 TLA+のようなより制約の強い形式仕様が役立っただろう。 しかし、1ページの擬似形式仕様を書くだけでも、大きな助けになった。

         Begin(T, coord):
           R(T) := HLC(coord).now()
           writeSet(T) := {}


         Read(T, N, key):
             N.HLC.tick(R(T)) //HLC advanced. So any write or commit after this is guaranteed to be at a higher ts
             return latest committed version of key with ts <= R(T)


         Write(T, N, key, value):
             N.HLC.tick(R(T))
             if LatestCommittedVersion(key).ts > R(T):
                 abort T
             place provisional intent for (key, value)
             writeSet(T) := writeSet(T) union {key}
      

この擬似形式仕様は、私の思考を明確にし、さらなる議論、実装、テストを通じた検証のための 良い基盤となった。

LLMとの協働

「コーディングを語彙構築として考える」ことは、LLMにとって重要な意味を持つ。 LLMは大量のテキストとコードから語彙を学習している。 それらは名前、API、ライブラリ、フレームワーク、イディオム、デザインパターン、実装構造の間の 繰り返し現れる関係性を学習する。Controller、Repository、Reducer、ConsensusModule、TransactionLog といった単語を見たとき、それらの名前は単なるラベルではない。 それらは既知のコード構造と期待される振る舞いとの関連性を伴う。

これが、LLMを扱う際に語彙が重要である理由である。私たちのプロンプトが 曖昧または一貫性のない言語を使っている場合、モデルは意図した設計を推測しなければならない。 コードベースが不明瞭な名前や一貫性のない概念を使っている場合、 モデルが従うべき安定した構造はほとんどない。 しかし、語彙が正確で一貫性があり、コードに具現化されている場合、 LLMは私たちの意図をより確実に有用な実装にマッピングできる。

認知負債

これはまた、LLM支援コーディングの特定の危険性——認知負債——を説明する。 認知負債は、言葉、抽象化、構造が、それらを使う人々によってその意味が十分に理解されないまま使われるときに蓄積される。 LLMは、大量のそれらしいコードを非常に速く生成できるため、このリスクを増幅させる。 生成されたコードには、Controller、Repository、Reducer、Factory、Transaction、Schedulerなどの 見た目には馴染みのある構造が含まれるかもしれない。コードはコンパイルされ、基本的なテストをパスするかもしれない。 しかし、チームがそれらの構造の背後にある概念モデルを理解していない場合、 コードベースは共有された理解なしに語彙を獲得したことになる。 問題はLLMがコードを生成したことではない。 問題は、開発者が理解を構築するよりも速くコードが語彙を導入したことである。 そのギャップが、認知負債の主要な要因の1つである。

共有された概念モデルとしてのコード

LLMによる抽象化の設計で議論したように、 コードを書くことには2つの深く絡み合った活動がある。抽象化の発見と適用である。 抽象化を発見することは、語彙を開発している場所である。 一度強力な語彙が構築されると、それは共有された概念モデルを表す。 そのモデルが存在するなら、コーディングの多くは、その概念モデルを使って ユースケースを構築することになる。ここで優れたライブラリと優れた基盤コードが輝く。 これは、プログラミング言語や環境の複雑さを隠そうとし、 私たちが構築した語彙に対してより英語に近いインターフェースを提供しようとする部分である。 これが機能する典型的な方法は、この語彙や抽象化の使用を容易にし、自然言語に近づける DSLを構築することである。

LLMはこの点で優れている。LLMは、抽象化の語彙に対する自然言語インターフェースを提供する。 最高の点は、抽象化の語彙の背後に実行可能なコードがある場合、それが LLMのミスを修正するための優れたガードレールとして機能することである。 優れた抽象化、実行可能な振る舞い、テスト、型、不変条件はすべて、 モデルを制約し、その出力をより有用にするのに役立つ。

語彙を使って外部DSLを構築することもできる。 LLMは、外部DSLの上に位置する自然言語インターフェースとして非常にうまく機能する。 私はPlantUMLのようなツールでLLMを大成功で使っている。これは当然のことである。LLMは本質的に 語彙をマッピングすることに最も優れているからだ。

その意味で、強力な基盤コードはLLMの時代においてさらに重要になる。 システムの語彙がうまく形成されたら、コーディングは生の構文を生成することではなく、 十分に発達した概念言語を使って信頼性の高いソフトウェアを構築することに比重が移る。

ハーネスおよびコンテキストとしてのコード自体

コンテキストエンジニアリングやハーネスエンジニアリングに関する多くの議論では、コードをブラックボックスとして扱い、 プロンプトに適切なコンテキストを提供したり、仕様、テスト、静的検証などで適切なハーネスを構築したりすることで、 コードが意図したとおりに構造化され動作することを保証する責任を外部で管理しようとする。 抽象化が明確に定義された語彙を形成する、よく構造化されたコードは、 それ自体がハーネスとコンテキストの最も重要な部分として機能する。 私は、よく設計されたコードがLLMと非常にうまく機能する成功事例を繰り返し見てきた。 より重要なのは、コードが明確なセマンティクスを持つ安定した抽象化で構築されている場合、 使用するLLMモデルをある程度自由に選択でき、プロンプトの精度をそれほど心配する必要がなくなることである。 コード構造と付随するテスト自体が、LLMの出力を信頼性が高く有用なものにするコンテキストとハーネスを提供する。

結論

コーディングの役割が消えるわけではない。しかし、それは変化している。

LLMがコード生成を安価にするにつれ、命令を書くという機械的な行為は中心的な位置を失いつつある。 より重要になるのは、概念モデルを明示的にし、適切な語彙を発見し、 反復、ドメインの専門知識、フィードバックを通じてその語彙を洗練することである。 これが、プログラミング言語が今後も深く重要であり続ける理由でもある。 私たちは生成されたコードを受動的なレビュアーであるべきではない。 コードを書く行為は、それ自体が私たちの思考の一部である。

コードは依然として機械への命令である。しかし、それはまた、理解のモデルでもある。 LLMの時代において、その2番目の役割はさらに重要になる。 コーディングの未来は、ただより多くのコードをより速く書くことではない。 それは、より良い概念モデル、より良い語彙、そして人間とLLMの両方がその上で作業できる より良い基盤を構築することである。