たなかこういちの開発ノート

システム開発に携わる筆者が、あれこれアウトプットするブログ

データ制約に統一され得る、型、契約、バリデーション

まとめ
 
プログラミング言語では「型」がドメインモデルを表現する重要ツールである。一方、モデリング観点からはデータ値に対する制約こそがある意味本質で、(主にNominal Subtypingが想定されるところの)型も制約の一手段なのだと気付く。
 
制約一般を型として扱う上でのツールとしては、Nominal Subtypingに限られること無く、依存型やStructural Subtypingを用いることができる。もう少しカジュアルには(=動的評価でよいなら)、型にDbC不変条件を常にセットしたものを考えられる。実装的には、バリデーションの一般的な仕組みをDbCを担うものとして扱う方式が取れる。目下の実現手段(=動的評価するフレームワーク)と理論的なところ(=依存型など静的評価を目指す)とを、宣言的プログラミングによる静的なセマンティックスと実行モデルの分離を以って繋いでみる。
 
〜・〜・〜
 
型、契約プログラミング、入力バリデーション。これらはモデル的には全てデータに対して制約(Constraint)を与えるもの、という観点で一つに統合され得る。
 
モデリング観点からはデータ制約こそがただ一つの本質である。型(※主にNominal Subtypingが想定されるところのもの)はむしろデータ制約のサブセットである。Integer型とは「数字のみで構成される」と制約されたデータ、DateTime型とはそういう構造を持つと制約されたデータ。もし「商品番号」というドメインクラス(※Doma用語だ)を作ったならば、それはそういう構造や関係を持つと制約されたデータ、が欲しかったからだ。
 
DbCの契約は、(制約観点で)型よりも拡張されている。DbCの契約というと事前条件が最初に想起されるかもしれないが、ここではまず(オブジェクトの)不変条件に注目するとわかりやすいだろう。あるオブジェクトの属性は総じて常に不変条件を満たすべきとされる。先の「商品番号」というドメインクラスは、Stringをベースにしただろうか、Integerをベースにしただろうか。ま、Stringベースだとして、例えば"[A-Z]{2}[0-9]{4}\-[0-9]{5,10}\-[A-Z]{0,2}"という不変条件を与える事としよう。ちなみに「商品番号」のこのコード体系は、その小売店の販売管理の都合に依っている(とする)。ドメインクラス、つまり型を定義しただけでは、この値内容の制約は表されていない。不変条件を付加してようやく、まさにドメイン=定義域が具体的に表現できたと云える。
 
この不変条件を、アサーションでしか用いないのはもったいない。入力バリデーションにも用いたい。アサーションとバリデーションは、単に、捉えるオブジェクトの粒度の違いでしかないと見なせる。ユーザー(エージェント)やその他のシステムがクライアントで、自システムにデータ値を受け入れるか否かをチェックするときバリデーションと云う。つまり、“システム間”でデータ交換するときのチェックをバリデーションと云っている。一つのプログラム内で、他のクラス・インスタンスがクライアントで、自インスタンスにデータ値を受け入れるか否かをチェックするとき、つまり、“システム内部”のモジュール間でデータ交換するときのチェックを、アサーションと云う。前者は外部インターフェースを挟んでいて、外部インターフェースを通して自己にデータ値を受け入れるようなときバリデーションといい、一つのプログラム内にて、スタティックまたはダイナミック・リンクされた他モジュールからデータ値を受け入れるようなときアサーションといっている、このように捉えられる。また、アサーションは“システム内”の問題なので、十分にテストが済んだら無効化する場合もある。バリデーションは“システム間”の問題なので、プロダクション運用時も無効化するようなことはあり得ない。ただこれも、その検査がどういった階層で為されるのかという違いに起因する運用モデルが異なるのみ、と見做す。
 
制約で、自オブジェクトのある属性が取り得る値範囲を定義するとき、なにも正規表現や最大値最小値で境界値を表現するに留める必要はない。先の「商品番号」に、「商品マスターに存在する」という制約が備わっていてもよい。もちろん、この制約評価を行うと、データーベースアクセスが発生する。DbCの契約ではあまり普通ではないが、入力バリデーションであれば普通のことだ。制約としても、当然に外部資源参照しての評価ロジックも想定する。
 
ちょっと補足しておくと、「商品番号」に「商品マスターに存在する」という制約を備えようというとき、実際には「商品番号」オブジェクトそのものではなく、その特定状態を表すサブクラス、例えば「登録済みの商品番号」に装備すべきだろう。別のサブクラス「未登録な商品番号」には、むしろ「商品マスターに存在しない」という制約を装備すべきである。
 
 
型について。そもそも型に何を期待しているのかを、実利面から問い直してみる。あるデータがある型であるか否かを問うことには、二つの実利がある。一つ目はデータ種の分別である。Domaドメインクラスが典型であるが、「商品番号」と「顧客番号」を取り違えていた場合、コンパイラーに検知してもらうことができる。「商品番号」、「顧客番号」が単にStringで表されていた場合は、IntegerではなくStringだという点では分別されているが、それ以上には分別されていない。
 
二つ目は、演算適用時の安全性である。例えば、そのデータがInteger型だという保証があれば、subtract演算が適用できることが保証されている。もちろん演算適用の安全性は、データ種の分別に強く依存してのこと。分別が保証されていれば、演算適用の安全が保証される、訳である。では、データが「商品番号」型だという保証があれば、`指定されるところの商品名を得る`演算が適用できることが保証されている、と言えるだろうか?答えは、「言えない」。ドメインクラス「商品番号」だというだけでは、`その商品番号が指定する商品の商品名を得る`演算の安全性は保証されない。保証するためには、「商品マスターに存在する」という制約が必要なのである。
 
静的型付けを指向または嗜好する人たちは、暗に明にかような実利を期待して型を使っているはずである。だとしたら、その実利を追求していくなら、(例えば Java言語の)型だけでは不足している機能がある、ということになる。制約は、その不足を補うために導入するのだ。
 
そうなると、「Integer」と「数字のみに制約されたString」にどういった違いがあるのだろうか?私は「違いはない」と結論づけている。Integer型とは、まさに「数字のみに制約されたString」だと再定義する。これを突き詰めて、型というものを次のように再定義してみる。「基底にOpaque型といったものがあって、それに次々に制約を課していったものが個々の型である」と。
 
言い換えると、型とは「特定の制約セットに名前をつけたもの」となる。その制約セットを満たす限り、その名前のデータ種だとみなせるし、その名前のデータ種に期待される演算適用の安全性が保証される、となる。
 
ここには、型を"is-a"と解釈するのではなく、"satisfies-that"と解釈する、というパラダイム転換がある。データそれ自身が自然に自分が何者であるかを表明するというより、データ利用側として、データが所定の条件を満たせばそういうデータ種だとして取り扱ってよいだろう、という考えである。
 
 
以上、自分の考えをまとめたものであるが、いくつか突っ込みどころがありそうなので考察してみる。
 
○ パラダイム転換なんて云ってるけど、"satisfies-that"って要はStructural Subtypingや依存型そのものでしょ?
 
多分その通りで、それらの利用価値をモデリング観点から捉え直した、のだ。
 
静的型付けには元よりNominal SubtypingとStructural Subtypingという二種類の考えがある。Java言語他、産業界で使われている多くの言語ではNominal Subtypingしかサポートしてない。Structural Subtypingは、継承によらず、構造の部分が一致すれば(=一部の属性やメソッドのシグネチャーが要求に一致すれば、特定のクラスやインターフェースやトレイトを継承していなくても)、当該部分型であるとされる。ScalaはStructural Subtypingを直接サポートしている。C++のテンプレートはStructural Subtypingの一つの実現だと私は理解している。
 
次に、値内容に踏み込んだ型のことを依存型という。同じ整数型だとしても保持する値が1か-10かで異なる型として扱おうというものである。コンセプトは分かるとしても、依存型をまともに実装してる処理系はAgdaとか定理証明の世界になってしまうようで、Java言語的な処理系の上に依存型のコンセプトをまともに実装するのは困難に思える。そこで、結局は評価は実行時に行われるアサーションやバリデーションだという実行モデルでよいなら、値に対する条件を「制約」として、制約の対象となるオブジェクトと構成上一体に記述できる処理系を想定することくらいはできるだろう。十分に宣言的なセマンティックスで制約を記述すれば、目下の評価の実行モデルは動的だが、将来は一定程度静的にできる、という含みも持たせられるだろう。
 
最後に、Structural Subtypingと依存型の統一について。依存型は一つのスカラ値(単純型)が特定の定義域にあることを云っている。Structural Subtypingは、あるコンポジット型が特定の属性を持っている、ことを云っている。いずれも、'between'とか'has'といった演算子で評価されるところの、型に対する「制約」、として捉えることで統一的に扱える。
 
そうして、型に対する制約としての依存型やStructural Subtypingをバリデーションやアサーションとして用いる、という話に戻ってくる。
 
○ 要はバリデーション大事、バリデーション・フレームワークがあればよいって話では?
 
道具の問題は解決すると思う。ただし、バリデーションと言っても、アプリの個別の入力要件に対応して、都度都度Formに対してバリデーションを定義するというよりも、ドメインのモデルの問題として、バリデーションの対象オブジェクト側(エンティティ側)に埋め込みたいのだ。用途としてはバリデーション類似だが、入力の受け入れ条件ではなく、エンティティ自身の有り様をエンティティ自身に埋め込むという点でDbC不変条件的であり、よって両者の概念を統一した扱いを目指す、という主張である。
 
○ ていうかダックタイピングじゃないの?
 
そもそもダックタイピングとは何か?私は、動的型付けの世界にStructural Subtypingを(一部)導入したもの、と捉えられると思っている。
 
○ バリデーション and/or アサーションの実行時評価でよいなら、動的型付けがいいのでは?
 
依存型 > Nominal Subtyping > Structural Subtyping > 動的型付け 、みたいな“型さ”の強度の序列が付けられるとして、依存型が実現できないからと云って、動的型付けまで退却してしまう必要も無いだろう。普通にNominalで静的検査出来るところはして、値内容に踏み込んだところのみ動的評価するという考えでよいだろう。
 
それと、Structural Subtypingとダックタイピングはおそらくほぼ同じものなのだが、静的型付け界からはStructural Subtyping、動的型付け界からはダックタイピング、として、お互いそれぞれの領分の話だと捉えられている。それで、静的型付け界がStructural Subtypingを云うと、動的型付け界は「やはり動的型付けの柔軟さが良いんじゃん」と云う。柔軟さが欲しいのだ、という指摘はその通りなのだが、ただ、静的型付けにおけるStructural Subtypingでは、そのような構造であることを(ad-hocに)宣言する、もしくはできるが、動的型付けにおけるダックタイピングでは、宣言しない、もしくはできない。可能な限り宣言し可能な限り静的検査しようするか、そうではないか、そういう指向もしくは嗜好の違いはある。
 
○ 制約ってモデリングでは普通のことでしょ?
 
データを定義するのに、(Nominalな)型だけでなく、値内容に対する制約を付加する試みは、モデリング的には普通に為されている。かつてUMLにまさにOCL(Object Constraint Language)という制約記述の方法が規定されたことがある。自動生成系、高速開発系ツールでは、制約やバリデーションのサポートは重要な機能要素である。ERモデルでは、参照制約・存在制約の分析が設計の要点である。
 
ただ、OCLは結局はドキュメンテーションの話だし、ツールはツールの処理系が動的検査をがんばるという話である。プログラマーとしては、もう少し“コード”に寄ったところに、コードでドメインモデルを表現できる体系がほしいのだ。
 
◆以上