先々月あたりに邦訳が出た『データ指向プログラミング』を読んだ。めちゃくちゃざっくり言うとオブジェクト指向における「データと振舞いの一体化」というドグマを手放させるもので、めちゃくちゃざっくり見るとC言語的な構造化プログラミングの世界に回帰するようにも見えることから、世間的には色々な評価があるらしい。
イミュータビリティが良い、みたいな話は重々承知なので適宜飛ばしながら読んだ。また、lodashまわりの話は単なるライブラリ紹介であり、特筆することがないので飛ばした。
こういう感じで言及されている
自分なりに読んで思ったことをまとめる。面白い本だったし、自分の思想にも近しいものがある。僕は普段ScalaやTSを書いているので、その観点からの感想が多いです。OOPの歴史に詳しいわけではないので、不足があったらすみません。
OOP以前の世界との差異
DOPはオブジェクトを分割してデータと振る舞いの蜜月関係を破壊するという。すると一見「OOP以前の手続き型的、構造化プログラミングの世界に逆戻りするだけでは」という意見になると思う。
しかし、DOPでは(明に触れられていないが)構造化プログラミング的世界観とはいくつか違う点があり、そこが重要な点となっている。
- イミュータブルなデータ構造の採用
- バリデーションとオブジェクト記法の発展
- 楽観的な状態管理
イミュータブルなデータ構造の採用
(自分はあまり通過していないので半分想像になってしまうが)構造化プログラミングの時代においてはミュータブルなデータ構造が基本であって、イミュータブルなデータ構造が使われることはあまりなかった。
他方、DOPではイミュータブルなデータ構造、特に永続データ構造(イミュータブルでありながら、高効率にデータを使い回す構造)を積極的に採用する。JavaScriptはそれ自体イミュータブルなデータ構造をネイティブに有しているわけではないが、各種のライブラリを利用してデータ構造やメソッドをラップすることでイミュータブル性を確保するという手法をこの本では紹介している。
イミュータブル性が提供する安全性が、データと振る舞いを無理なく分離させることができている印象だ。これがミュータブルに行われていたとしたら、同じ参照が示すものが同じである保証はどこにも無くなり、あっという間にデータは滅茶苦茶になるだろう。これは従来の構造化プログラミングとは決定的に異なる点だ。
ちなみに僕がよく使っているScalaはVector
として永続データ構造が言語機能の中に標準的に組み込まれており、何もせずに使うことができる:
val xs: Vector[Int] = Vector.fill(100000000)(42) // 1億要素のVectorを作成する xs.updated(50000000, 666) // 永続データ構造で構築されているため、要素の置換は一瞬で実行される xs // xs自体は変化しない
バリデーションとオブジェクト記法の発展
DOPはデータ構造を継承関係から解放し、その血筋から自由にする。こう書くと革命的な感じですね。DOPは構造的部分型を直接使うわけではないので、「公称型nominal typingからの脱却」と言ったほうが適切かもしれない。一般的なOOPでは継承関係を用いた実装とデータの共有が多用されがちだが、DOPではこれらの継承による型関係は一種のしがらみと見て排除する。その代わりに辞書構造に回帰し、データの特定のプロパティに(安全性を犠牲にして)直接アクセスしようとする。要するに、普段われわれがJSONにアクセスするような感覚でデータを取り扱う。
当然ながら辞書構造に回帰する以上安全性がかなり損われる。実行時にフィールドが存在しなかったり、意図しない値が入っていたりするおそれがあるからだ。この本によれば、JSON Schemaを利用してドメインやインターフェイスの境界面でバリデーションを行うことでそのような問題は発生しなくなる、という立て付けだったが、個人的には楽観的すぎるのではないかと感じた。
個人的には、TypeScriptなどの諸言語が採用している構造的部分型を採用するのがバランスが取れていると思う。ScalaはJava風のオブジェクトシステムを持っているため通常は公称型を使うが、record4sなどの構造的部分型を導入するライブラリがあるため、Scalaの強力な型安全性を担保にしつつ、かなり柔軟にデータを扱える。
//> using dep "com.github.tarao::record4s:0.9.1" import com.github.tarao.record4s.% val me = %(name = "windymelt", born = 1993) // 補完が効く me.name // => "windymelt" val me2 = me ++ %( favorite = %( lang = "scala", fruit = "peach", os = "FreeBSD", ) ) me2.favorite.fruit // => "peach"
また、構造的部分型にも通じる話だが、JS/TSではあるオブジェクトを表現するための記法が非常に洗練されていて、おそらくどの言語よりも(唯一の例外であるLISPを除いて)自由にオブジェクトを書くことができる。この手軽さがなければ、DOPを実践するのはかなり辛い。
楽観的な状態管理
この本にはRDBMSの話があまり出てこない。というのも、DOPに忠実なアプリケーションではデータ操作と状態管理は別々のモジュールに分離されていて、データのミューテーションを行う際は、まずデータを操作した後で、最終的な状態をコミットする処理をアプリケーション内の一箇所引き受けて、楽観的なロック制御で世代管理をすることをまず推奨しているためだ。その次に高度な話題としてRDBMSが登場する。普段われわれ(誰?)はDBに繋いであれこれするのに慣れきっているため、これはかなり異様に感じた。
加えて、RDBMSを使うときにデッドロックなどしたらどうするのか、という話が無かったのが気になった。自分の理解が及んでいないのか、あまり納得いっていない。普段はウェブサービスばかり書いているから、あまり想像がつかないのかなあ。
全体的な感想
自分は普段ScalaとかTSで開発している。JS/TSではDOP的な思想を貫徹するのはかなり大変な道程になるだろうと思うが、Scalaだとこの本で解説された思想はだいたい言語機能やライブラリとして取り込まれていて、普通にScalaを書いていたら(状態制御まわりの話以外は)だいたい実践している人が多いのではないか。
- イミュータブルなデータ
- 勝手に言語機能でそうなっている。何もしなくても永続データ構造が使える。
- 関数型的な書き方をすると、自然と状態は排除されていき、テストもしやすくなっている。
- コードとデータの分離
- ScalaではJava的な書き方も可能だが、流儀としては
case class
にデータだけを持たせて、あとは型クラスやサービスメソッド的な役回りの機構に処理させることが多いと思う。 Either
やTuple
などが充実しているため、データはデータとしてそのまま調理する風習が強い。
- ScalaではJava的な書き方も可能だが、流儀としては
- バリデーション
- そもそも型が強いので、あまりバリデーションに頼らずに済んでいる。
- record4sなどの構造的部分型を使うライブラリを利用して、型レベルでフィールドの存在が保証できる。
- refinedなどのバリデーションライブラリを利用して、型レベルで値の特性を保証できる。
- ジェネリックな関数
- 型クラスの思想そのもの。
- 状態管理
- RDBMSにやらせることが多いように思う。
- より手前側の、アプリケーション内で並行処理をやる、といったレベルの状態管理では、Cats Effectなどの非同期ライブラリを活用して粒度の高い型安全で使いやすいインターフェイスを利用する。
- 情報パス
- データ中の特定のフィールドを指し示すためのフィールド名の配列のことを情報パスと呼んでいる。
- オブジェクト中の入り組んだ場所を取り出したり変更できる。
- Opticsがやりたかったことだなぁ、という感想になった。ScalaではMonocleというライブラリがOpticsを実装する。
- OOPのGetter/Setterを再解釈し、合成可能なGetter/Setterなどを実装しているのがOpticsという枠組みです。
全体を通して、データに回帰せよというメッセージを強く感じた。その点については強く共鳴するところがあった。
参考文献
www.slideshare.net