5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

アナパタの知識レベルの制約を型レベルで書いてみる

Last updated at Posted at 2018-03-26

概念モデリングの古典『アナリシスパターン』(以下アナパタ)では多くの「モデリングの原則」が紹介されているが、その中でも「知識レベルと操作レベルの分離」は特に有名で、ドメイン駆動設計にも取り入れられている。

概念のモデルなので実装の手法にはいろいろあるが、今回は Scala + shapeless を使った型レベルプログラミングを試してみる。

お題

アナパタ掲載の各種パターンには、随所に「知識レベルの分離」が取り入れられているが、ここでは最初に知識レベルが導入される「責任関係(Accountability)」(図2.9)を実装する。

Accountability_Fig2_9.png

特にコメント部分の制約の表現に、shapeless の CoproductSelectorを活用する。

バージョンと動くコード

パーティ型 - PartyType

知識レベルのパーティ型は、PartyTypeクラスとその派生クラスで表現する。これらは型パラメータとしてのみ現れ、コンパイル時に整合性がチェックされるが、インスタンスが作られることはない。

sealed trait PartyType

sealed trait Region   extends PartyType
sealed trait Division extends PartyType

sealed trait Doctor   extends PartyType
sealed trait Team     extends PartyType
sealed trait Patient  extends PartyType

Java などの感覚だと、Classクラスや類似のメタクラスでパーティ型を表現し、そのインスタンスの集合を通常のコレクションに入れて包含関係を実行時にチェックしたり、あるいはリフレクションを使って継承関係を調べたりしたくなるかもしれないが、ここではその方式は採らない。

パーティ - Party

操作レベルでパーティ型に対応する**パーティ(Party)**は、以下のように書いた。

型パラメータとして付与されたT <: PartyTypeは、Partyクラスの中で使われることもなく、コンパイルが通れば消えてしまうファントムタイプとなるが、こうした方式でも、図2.9の「あるパーティについてパーティ型が一つだけ決まる」という対応関係は表現できる。

case class Party[T <: PartyType]()

以下のようにインスタンスを作る。

val johnSmith     = Party[Patient]()
val renalUnitTeam = Party[Team]()
val markThursz    = Party[Doctor]()

val bostonRegion       = Party[Region]()
val cappuccinoDivision = Party[Division]()

責任関係 - Accountability

主役の**責任関係(Accountability)**は操作レベルに置かれ、委任者(commissioner)の分と責任者(responsible)の分と、それぞれ一つずつパーティを参照する。後述の責任関係型との対応は型パラメータで与えられるが、これもファントムタイプになる。

  trait Accountability[AT <: AccountabilityType, C <: PartyType, R <: PartyType] {
    val commissioner: Party[C]
    val responsible : Party[R]
  }

ActionTimePeriodは割愛した。

責任関係型 - AccountabilityType

**責任関係型(AccountabilityType)**は知識レベルで定義され、委任者の型の集合(Commissioners)と責任者の型の集合(Responsibles)を参照する。

上述の通り、パーティ型を表すメタな型は定義しない趣向なので、通常のコレクションのようなデータ構造は使わない。その代わり shapelessのCoproductを用いて、型として型の集合を表現する。

trait AccountabilityType {
  type Commissioners <: Coproduct
  type Responsibles  <: Coproduct

  def instance[C <: PartyType, R <: PartyType](
    aCommissioner: Party[C],
    aResponsible: Party[R]
  )(implicit
    sc: Selector[Commissioners, C],
    sr: Selector[Responsibles, R]
  ): Accountability[this.type, C, R] =
    new Accountability[this.type, C, R] {
      val commissioner: Party[C] = aCommissioner
      val responsible:  Party[R] = aResponsible
    }
}

メソッドinstanceは、与えられた委任者と責任者からAccountabilityインスタンスを生成するが、このとき 図2.9 の制約がコンパイラにチェックされる。

  • $x.commissioner.type \in x.type.commissioners$
  • $x.responsible.type \in x.type.responsibles$

ここで、型の集合の要素であることを表現するために、shapeless のSelectorを使っている。

AccountabilityType自体のインスタンス生成コードでも、(1)委任者/責任者に一つ以上の型が含まれていて(IsCConsを使用)、(2)それがパーティ型の派生クラスでなければならない(Unifierを使用)といった制約が、コンパイル時にチェックされる。

object AccountabilityType {
  def apply[C <: Coproduct, R <: Coproduct] = new {
    def apply[PC <: PartyType, PR <: PartyType]()(
      implicit  consC:    IsCCons[C],
                consR:    IsCCons[R],
                unifierC: Unifier.Aux[C, PC],
                unifierR: Unifier.Aux[R, PR]
    ) = new AccountabilityType {
      type Commissioners = C
      type Responsibles  = R
    }
  }
}

Accountabilityの生成時にもAccountabilityTypeの生成時にも、implicit パラメータで与えられたオブジェクトそのものは使ってはいないが、コンパイラによる implicit の解決を、制約条件が充足されていることの証明(proof)として利用している。

インスタンスは以下のように生成する。サンプルとして委任者={地域}、責任者={部門}となるような地域構造型と、委任者={患者}、責任者={医師, チーム}となるような患者同意型を定義した。

// 地域構造型
val regionStructureType = AccountabilityType [
  Region   :+: CNil,
  Division :+: CNil
]()

// 患者同意型
val patientConsentType = AccountabilityType [
  Patient :+: CNil,
  Doctor  :+: Team :+: CNil
]()

以下のような責任関係型の定義は、implicit が見つからない、つまり制約が満たされないためコンパイルに失敗する

// 委任者が空
// val ngType1 = AccountabilityType [CNil, Division :+: CNil]()

// 責任者に PartyType でないものが含まれている
// val ngType2 = AccountabilityType [Region :+: CNil, String :+: CNil]()

責任関係インスタンスを得るには、上述の責任関係型のinstanceメソッドを用いる。

// 患者同意型: 患者 John Smith から医師 Mark Thursz への同意
val pc1 = patientConsentType.instance(johnSmith, markThursz)

// 患者同意型: 患者 John Smith から腎単位チームへの同意
val pc2 = patientConsentType.instance(johnSmith, renalUnitTeam)

// 地域構造型: カプチーノ部門はボストン地域に含まれる
val r = regionStructureType.instance(bostonRegion, cappuccinoDivision)

以下の例は、制約を満たさないためコンパイル時にエラーとなる。

// 患者から患者への同意
// val ng1 = patientConsentType.instance(johnSmith, Party[Patient])

// 部門が地域を含む
// val ng2 = regionStructureType.instance(Party[Division], Party[Region])

##所感

  • 当然ながら静的な制約に限られる手法になるが、静的に表現できる、あるいはすべき場合には、メタクラス+リフレクションを使った実行時制約チェックからの例外送出などよりも安全確実に書けそう。
  • というか、実行時にメモリ上の何かを調べて制約を満たしているか否かをチェックする方法(HOW)を記述する命令型な考え方とちがって、コンパイラでも整合性をチェックできるような型として対象が満たすべき論理的制約(WHAT)を記述しているわけだから、大幅に宣言型なプログラムになる。
  • もちろん DDD が称揚する declarative style にもつながる。
  • 工夫すれば、型の包含関係にとどまらず、もっと複雑な知識レベルの制約も型で表現できそう。
  • 制約を満たさない場合のコンパイルエラー時のメッセージは、若干わかりにくい。
  1. IntelliJ で動かすときは Run Type を REPLでは なく Plain に設定

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?