Created
January 10, 2016 19:56
-
-
Save michaeldiamant/7efbac9a9735a74abd96 to your computer and use it in GitHub Desktop.
Use dependent types to represent a relationship between a command and its possible failures and successful outcomes (i.e. events). This representation encodes the relationship into the data types rather than a service-like interface.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
object E1 { | |
// Encoding a 1:1 command to event example | |
sealed trait Event | |
case class E1(i: Int) extends Event | |
case class E2() extends Event | |
sealed trait Intent[Z <: Event] { | |
type Out = Z | |
} | |
case class C1(id: Int) extends Intent[E1] | |
case class C2(id: Int) extends Intent[E1] | |
case class C3(id: Int) extends Intent[E2] | |
def handle[Z <: Event](i: Intent[Z]): i.Out = i match { | |
case C1(id) => E1(id) | |
case C2(id) => E1(id) | |
case C3(id) => E2() | |
} | |
val r1: E1 = handle(C1(5)) | |
val r2: E2 = handle(C3(4)) | |
} | |
object E2 { | |
// Encoding 1:1 relationship with added capability of expressing multiple | |
// rejects for a particular intent by adding specific sealed trait. | |
// The shortcoming here is that a reject common to all intents cannot be | |
// shared. The reject would need to be duplicated for all reject traits. | |
sealed trait ApiReject | |
sealed trait C1ApiReject extends ApiReject | |
case class Duplicated(i: Int) extends C1ApiReject | |
case class InvalidName(i: Int) extends C1ApiReject | |
sealed trait C2ApiReject extends ApiReject | |
case class NotFound(i: Int) extends C2ApiReject | |
sealed trait Intent[R <: ApiReject] { | |
type Reject = R | |
} | |
case class C1(i: Int) extends Intent[C1ApiReject] | |
case class C2(i: Int) extends Intent[C2ApiReject] | |
def handle[R <: ApiReject](c: Intent[R]): List[c.Reject] = c match { | |
case C1(x) => Duplicated(x) :: InvalidName(x) :: Nil | |
case C2(x) => NotFound(x) :: Nil | |
} | |
val r1: List[ApiReject] = handle(C1(1)) | |
val r2: List[C1ApiReject] = handle(C1(1)) | |
// val r2: List[C2ApiReject] = handle(C1(1)) // fails | |
} | |
object E3 { | |
// Advancing E2 to support sharing a reject across multiple intents while | |
// still allowing each intent (i.e. command) to define a set of possible | |
// rejects as a coproduct | |
import shapeless._ | |
sealed trait ApiReject | |
case class Duplicated(i: Int) extends ApiReject | |
case class InvalidName(i: Int) extends ApiReject | |
case class NotFound(i: Int) extends ApiReject | |
type C1ApiReject = Duplicated :+: InvalidName :+: CNil | |
type C2ApiReject = NotFound :+: CNil | |
// Will never be used because it does not define a Rejectable instance | |
type C3ApiReject = String :+: CNil | |
sealed trait Rejectable[A] | |
implicit object C1Rejectable extends Rejectable[C1ApiReject] | |
implicit object C2Rejectable extends Rejectable[C2ApiReject] | |
sealed trait Intent[R <: Coproduct] { | |
type Reject = R | |
type Foo = Rejectable[R] | |
} | |
case class C1(i: Int) extends Intent[C1ApiReject] | |
case class C2(i: Int) extends Intent[C2ApiReject] | |
case class C3(i: Int) extends Intent[C3ApiReject] | |
def handle[R <: Coproduct : Rejectable](c: Intent[R]): c.Reject = c match { | |
case C1(x) => Coproduct[C1ApiReject].apply(Duplicated(x)) | |
case C2(x) => Coproduct[C2ApiReject](NotFound(x)) | |
case C3(x) => Coproduct[C3ApiReject]("") | |
} | |
object f extends Poly1 { | |
implicit val caseD = at[Duplicated](e => e.i) | |
implicit val caseX = at[InvalidName](e => e.i) | |
} | |
// val r1: List[ApiReject] = handle(C1(1)) | |
val r2: C1ApiReject = handle(C1(1)) | |
val res: Int = r2.fold(f) | |
// val r3: List[C1ApiReject] = handle(C3(1)) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment