We will cover
- A service algebra.
- A service interpreter.
- Property laws of interpreter.
- A usage of shapeless.
- And a bit more on shapeless.
We will walk through a solution in practice, and we don't intend to advocate any libraries.
trait Service[F[_], A, {
def create: ServiceResult[F, Store[A], ServiceError[CreateError], A]
// read, readAll, delete
}
object Service {
// Given a store,
// give some error E or result A, under an effect F.
// To directly unwrap value/error,
// we lift to EitherT.
type ServiceResult[F[_], C, E, A] =
EitherT[Kleisli[F, C, ?], E, A]
}
The interpeter works for any entity with Validation
, and that's the first step of create
or update
.
class ServiceInterpreter[F[_] : Monad, I, A : Validation] {
def create(entity: A): ServiceResult[F, Store[A], ServiceError[CreateEntityError], A] =
...
store =>
(for {
valid <- EitherT {
log(debug(s"Started validating the entity ${entity.shows}")) >>
entity
.validate.disjunction
.leftMap(CreateEntityError.InvalidFields).pure[F]
}
ex <- store.insert(valid).bimap(CreateEntityError.fromInsertError, _ => validatedExchange)
} yield ex).run
...
- We test service layer with all combinations of results a database can produce.
case class Dummy(string: String)
val service = new ServiceInterpreter[Id, Int...]
implicit val res: Arbitrary[StoreResult] = ???
implicit def valid[A]: Arbitrary[Validation] = ???
forall {r: StoreResult, valid: ValidationNel[Error, A], in put: Dummy } {
...
str <- store.insert(e).run
ser <- entityService.create(e).run(store)
exp <- str.bimap(ServiceError.CreateEntityError.fromInsertError, _ => r)
...
ser mustEqual exp
// Yet to talk on Validation
Validation is encoded as a simple type class. Entities can run ServiceInterpreter if they have a validation instance.
trait Validation[A] {
def validate(a: A): ValidationNel[ValidationError, A]
}
object Validation {
def createInstance[A](f: A => ValidationNel[ValidationError, A]): Validation[A] =
a => f(a)
implicit class Validator[A](a: A) {
def validate(implicit validator: Validation[A]): ValidationNel[ValidationError, A] =
validator validate a
}
case class ValidationError(msg: String)
}
- We test interpreter's behavior on the combination of validation results as property based tests. We include this into the same property based test.
- This enables us to test interpreter's behavior on almost all combinations of validation results and store results. Ex: Verify the behavior of service layer if it fails early.
import org.scalacheck.ScalacheckShapeless._
case class Dummy(field1: Field)
case class Field(value: Int)
implicit val arb1: Arbitrary[Field1] = Arbitrary { Gen.oneOf(0, 1).map(Field) }
// Using ScalacheckShapeless
// we skip deriving arbitrary for Dummy
implicit val validField: Validation[Field] =
Validation.createInstance(x => if (x.value === 0) x.successNel[ValidationError] else ValidationErrror("1 is wrong !").failureNel[Dummy]
// Using com.telstra.dxhub.ValidationSupport._,
// we skip deriving valiation instance for Dummy.
Dependency injection at server module:
// At server layer, we decorate the types. Define the store
// Doobie store is an interpreter of Store algebra handling running the queries.
private val exchangeStore = DoobieStore[IO, Exchange]
private val envStore = DoobieStore[IO, Environment]
private val topologyStore = DoobieStore[IO, Topology]
private val topVersionStore = DoobieStore[IO, TopologyVersion]
// Service indsstances
private val exchangeService = ServiceInterpreter[IO, Exchange]
private val envService = ServiceInterpreter[IO, Environment]
private val topologyService = ServiceInterpreter[IO, Topology]
private val topVersionService = ServiceInterpreter[IO, TopologyVersion]
// Run it ! - May be pass the function to corresponding controllers
exchangeService.create(_).run(exchangeStore)
envService.create(_).run(envStore)
envService.update(_).run(envStore)
...
No.That works only if all the entities have a
- validation instance - handled by us using Shapeless
- scalaz Show instance - handled by us using Shapeless
- A Doobie Composite instance - handled by Doobie using Shapeless
There is a bit more going on on with our show support that uses LabelledGeneric
in shapeless.
We will dig into it later and focus on only Validation that uses Generic
in shapeless.
case class X(a: Int, b: String, c: Double)
// Create an instance for Int, String and Double
implicit val validateInt: Validation[Int] =
a => if (a > 100) ValidationError("a is greater than 100").failureNel[Int] else a.successNel[ValidationError]
implicit val validateString: Validation[String] =
_.successNel[ValidationError]
implicit val validateDouble: Validation[Double] =
a => if (a > 200 ValidationError ("c is greater than 200").failureNel[Double] else a.successNel[ValidationError]
given a Validation for every H in HList, we derive Validation for HList
implicit def hListValidation[H, T <: HList](
implicit hValidation: Validation[H],
tValidation: Validation[T]
): Validation[H :: T] = {
case (h :: t) =>
(hValidation.validate(h) |@| tValidation.validate(t)) { _ :: _ }
}
// That is always a success
implicit val validateHNil: Validation[HNil] =
_.successNel[ValidationError]
given a Generic of A, given a Validation for HList of Generic[A], we derive Validation for A
implicit def validateA[A, R](
implicit gen: Generic.Aux[A, R],
ev: Validation[R]
):Validation[A] = a => ev.validate(gen.to(a)).map(gen.from)
Shapeless doc says:
Given a type A and an HList type R, an implicit Generic to map A to R, and the type class instance for R, create a type class instance for A
-
We have a similar type class derivation for Show that is based on
LabelledGeneric
,Witness
and Shapelesstaggers
. -
It gives a consistent representation of all entities and value objects in logs. It comes with zero extra code as
scalaz
provides show instance for primitives out of the box. -
We can override the behavior of show, such as for hide secrets.
-
We will cover more on this later.
The code is in: https://medium.com/@afsal.taj06/shapeless-for-logging-work-in-progress-2cf0a40d95c
- To avoid over defensive compiler search, we use
Lazy
construct in our implementation.
implicit def hlistValidate[H, T <: HList](
implicit hEncoder: Lazy[Validation[H]],
tEncoder: Validation[T]
): Validation[H :: T] =
Validation.createInstance {
case h :: t => (hEncoder.value.validate(h) |@| tEncoder.validate(t)){_ :: _}
}
implicit def genericValidation[A, R](
implicit gen: Generic.Aux[A, R],
env: Lazy[Validation[R]]
): Validation[A] =
a => env.value.validate(gen.to(a)).map(gen.from)
Similar to the automatic type class derivation for products using HList
we can
derive type class instances for ADTs
using H :+: T
(Coproducts).
Define type class instance for `Coproduct`
by defining instance for `CNil` and `H :+: T`
If we have an instance of `H :+: T`,
we can validate a `Entity` that could
be Entity1 or Entity2
- It was quick to get everthing working
- Team never had to spend anytime trying to debug anything related
- Manipulation, inspection and traversals at type level.
- Rho, circe, doobie, avro4s ... we are already with Shapeless!
- Less boiler plate.
- macros and compile time
Reading/Learning shapeless code uncovers a lot of concepts and patterns in Scala and we can bring the knowlwedge into our code base.
And then, we may choose to not use it !