開発

SICPデータ駆動プログラミングをHaskellで実践

SICPのデータ駆動プログラミング手法をHaskellで実装する試み。タグ付きデータの限界を超え、拡張性の高い設計を実現する方法を解説。

9分で読める SINGULISM 編集チームが確認・編集

SICPデータ駆動プログラミングをHaskellで実践
Photo by Mohammad Rahmani on Unsplash

SICP(Structure and Interpretation of Computer Programs)は、計算機科学の古典的教科書として広く知られている。その第2章ではデータ抽象化の手法が詳細に論じられており、特に2.4.3節ではデータ駆動プログラミング(data-directed programming)が扱われている。Lobstersのentropicthoughts.com by kqrの記事では、この概念をHaskellで実装する試みが紹介されている。

SICPとデータ駆動アプローチ

SICPはAbelsonとSussmanによって執筆され、1996年にMIT Pressから出版された。本書は「Wizard Book」の愛称でも親しまれ、多くのプログラマに影響を与えてきた。しかしその膨大な内容のすべてを熟読することは容易ではなく、著者は関心のある部分に焦点を当てて読み進めるアプローチを取っている。

前回の記事では、Haskellにおけるタグ付きデータ(tagged data)の実装が扱われた。しかしSICPの著者らは、タグ付きデータが最善のアプローチであるとは考えていない。新しいデータ表現を追加するたびに、既存の全操作を修正する必要が生じるからだ。この問題を解決するために提案されるのが、データ駆動プログラミングである。

複素数の4つの操作

複素数は、直交座標形式(実部と虚部)と極座標形式(大きさと角度)の2種類で表現できる。表現形式にかかわらず、複素数からは以下の4つの量を取得できる必要がある。

  • 直交座標形式の実部(real part)
  • 直交座標形式の虚部(imaginary part)
  • 極座標形式の大きさ(magnitude)
  • 極座標形式の角度(angle)

前回のタグ付きデータによる実装では、関数がタグを検査して適切な表現を選択していた。この方法でも動作はするが、新しい表現を追加するたびに4つの操作すべてを変更しなければならない。

タグ付きデータの限界

タグ付きデータアプローチでは、以下のようなコードで型タグを付与する。

attach_tag tag contents = (tag, contents)
type_tag (tag, _) = tag
contents (_, value) = value

直交座標表現と極座標表現を判別するには、タグ文字列を比較する。

is_rectangular z = type_tag z == "rectangular"
is_polar z = type_tag z == "polar"

make_rectangular re im = (attach_tag "rectangular" (re, im))
make_polar r a = (attach_tag "polar" (r, a))

この方法の問題点は、新しい表現形式を追加するたびに、既存の全操作関数に条件分岐を追加する必要が生じることだ。これは開放/閉鎖原則(Open/Closed Principle)に反する設計と言える。

データ駆動プログラミングの核心

AbelsonとSussmanが提案するデータ駆動アプローチでは、操作のテーブルを暗黙的に宣言し、そこから操作を取得・格納するための getput という2つの関数を使用する。

Haskellでは、Stateモナドを用いて以下のように実装できる。

put op tag fn = State.modify (Map.insert (op, tag) fn)
get op tag = State.gets (Map.lookup (op, tag))

このテーブルを使い、直交座標表現用の操作をインストールする。

install_rectangular = do
 put "real_part" "rectangular" (\(re, _) -> re)
 put "imag_part" "rectangular" (\(_, im) -> im)
 put "magnitude" "rectangular" (\(re, im) -> sqrt (re^2 + im^2))
 put "angle" "rectangular" (\(re, im) -> atan2 im re)

同様に、極座標表現用の操作もインストールする。

install_polar = do
 put "real_part" "polar" (\(r, a) -> r * cos a)
 put "imag_part" "polar" (\(r, a) -> r * sin a)
 put "magnitude" "polar" (\(r, _) -> r)
 put "angle" "polar" (\(_, a) -> a)

データ駆動アプローチの利点

このアプローチの最大の利点は、新しい表現形式を追加する際に既存のコードを変更する必要がない点にある。新しい表現用の操作を別途インストールするだけで、既存のジェネリック操作が自動的に新しい表現を扱えるようになる。

SICPのテキストでは、さらに apply_generic という汎用適用関数を使って、操作名とデータから適切な実装をテーブルからルックアップする仕組みが示されている。

ソフトウェア設計への示唆

データ駆動プログラミングは、今日のソフトウェア設計にも重要な示唆を与える。プラグインアーキテクチャや依存性の逆転、戦略パターンなど、現代の設計パターンの多くはこのアイデアを発展させたものだ。

Haskellのような関数型言語では、型クラスを用いたアドホック多相がデータ駆動アプローチの代替手段として広く使われている。しかし、型クラスがコンパイル時に解決されるのに対し、データ駆動プログラミングは実行時に操作をディスパッチする点で異なる。この実行時ディスパッチの柔軟性は、動的な拡張が必要なシステムにおいて価値を発揮する。

例えば、AIエージェントに開発工程を教えるagent-skills登場のようなシステムでも、プラグイン可能な操作テーブルの設計思想は応用可能だ。エージェントに新しいスキルを追加する際、既存の実装を変更せずに拡張できる点で、データ駆動アプローチは有効な設計選択肢となる。

編集部の見解

データ駆動プログラミングのHaskell実装は、古典的な教科書のアイデアを現代の関数型言語でどのように具現化できるかを示す好例だ。短期的には、SICPを学習中のHaskellプログラマにとって、この実装例はタグ付きデータからデータ駆動アプローチへの移行を理解する助けとなる。また、既存のHaskellコードベースにおいて、型クラスでは表現しづらい動的な拡張性が必要な場面での代替手法として参照される可能性がある。 長期的な視点では、データ駆動プログラミングの思想が現代のソフトウェアアーキテクチャに与える影響は依然として大きい。プラグインシステムやマイクロサービスのサービスディスカバリ、ルーティングテーブルなど、実行時に操作を解決する仕組みの根底には同じ原理が流れている。Haskellの型安全性を保ちながら、このような動的ディスパッチを実現する方法論は、より洗練された形で進化していくだろう。 編集部としては、関数型言語において型クラスとデータ駆動アプローチの使い分けをどのように判断すべきか、という論点が残ると考える。

参考

よくある質問

データ駆動プログラミングと型クラスの違いは何か
型クラスはコンパイル時に操作が解決されるのに対し、データ駆動プログラミングは実行時に操作テーブルからディスパッチする。型クラスは静的な型安全性を提供するが、新しい操作を実行時に追加する柔軟性はデータ駆動アプローチの方が高い。
SICPは現在も学習する価値があるか
SICPの内容は出版から30年近く経過しているが、計算機科学の基礎概念を深く理解する上で今なお価値が高い。特にデータ抽象化や手続きの第一級オブジェクトとしての扱い、評価戦略などの概念は、現代のプログラミング言語設計の理解に直結する。
Haskellでデータ駆動プログラミングを実装する際の注意点は何か
Stateモナドを用いた操作テーブルの管理は、参照透過性を一部犠牲にする。純粋関数型の原則を重視する場合は、型クラスや代数的データ型を用いた代替設計を検討すべきだ。また、テーブルのキーとして文字列を使用するため、実行時の型エラーに注意が必要である。
出典: Lobsters

コメント

← トップへ戻る