Skip to content

Instantly share code, notes, and snippets.

@afsalthaj
Last active June 11, 2019 01:25
Show Gist options
  • Save afsalthaj/93cce08b9c192199428a0c83a2f3bbb7 to your computer and use it in GitHub Desktop.
Save afsalthaj/93cce08b9c192199428a0c83a2f3bbb7 to your computer and use it in GitHub Desktop.

A walk through

algebra >>= interpreters >>= laws >>= typeclass >>= shapeless >>=

We cover:

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.


Our Service Algebra

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] 
}

Our Service Interpreter

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
        
    ...

Properties of ServiceInterpreter

  • 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

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)
}

Property based tests revisited.

  • 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.

Test validations

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.

How did we use Interpreter?

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)
  ...


Is that it?

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.


Example

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]



Validation for H :: T

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] 
 
  

Validation for any product

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)
  

Strategy

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


Show instances with Shapeless for Logging.

  • We have a similar type class derivation for Show that is based on LabelledGeneric, Witness and Shapeless taggers.

  • 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


Lazy of Shapeless

  • 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)


Automatic type class derivation for Coproducts (not used in dx-hub-api)

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

Good and Bad of Shapeless

Good
  • 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.
Bad
  • macros and compile time

What if we still hate shapeless ?

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 !


https://medium.com/@afsal.taj06/shapeless-and-applicative-functors-together-a-typical-use-case-433111f0a69f

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment