Algebras

Hammock relies heavily on free monads for declaring its main algebras and keeping things pure.

HttpF algebra

The HttpF algebra is the main pillar of Hammock. It has operations for all the HTTP verbs, and all operations have a single member req of the type HttpRequest. Then all these HttpRequests are interpreted with an InterpTrans.

MarshallF algebra

Our marshalling algebra only has one operation, unmarshall, that transforms an Entity into an A for which a Decoder exist.

Also, the marshalling package provides a natural transformation MarshalF ~> F, for arbitrary F[_] for which there exist an evidence of Sync.

Integrating Hammock with other algebras

Imagine you were already using free monads for the design of your application:

As always, start with some imports

import cats._
import cats.free._
import cats.data._
import cats.effect.Sync

Then, we need to start defining our algebras. Here’s is the algebra related to logging

Log

object Log {
  sealed trait LogF[A]
  case class Info(msg: String) extends LogF[Unit]
  case class Error(msg: String) extends LogF[Unit]

  class LogC[F[_]](implicit I: InjectK[LogF, F]) {
    def info(msg: String): Free[F, Unit] = Free.inject(Info(msg))
    def error(msg: String): Free[F, Unit] = Free.inject(Error(msg))
  }

  object LogC {
    implicit def logC[F[_]](implicit I: InjectK[LogF, F]): LogC[F] = new LogC[F]
  }

  def interp[F[_]: Sync]: LogF ~> F = new (LogF ~> F) {
    def apply[A](logF: LogF[A]): F[A] =  logF match {
      case Info(msg) => Sync[F].delay(println(s"[info]: $msg"))
      case Error(msg) => Sync[F].delay(println(s"[error]: $msg"))
    }
  }
}
// defined object Log

Console IO

And this one does IO.

object IOEff {

sealed trait IOF[A]
  case object Read extends IOF[String]
  case class Write(msg: String) extends IOF[Unit]

  class IOC[F[_]](implicit I: InjectK[IOF, F]) {
    def read: Free[F, String] = Free.inject(Read)
    def write(str: String): Free[F, Unit] = Free.inject(Write(str))
  }

  object IOC {
    implicit def ioC[F[_]](implicit I: InjectK[IOF, F]): IOC[F] = new IOC[F]
  }

  def interp[F[_]: Sync]: IOF ~> F = new (IOF ~> F) {
	// this could be implemented using scala.io.StdIn, for example
	def readline: String = "line read!"

    def apply[A](ioF: IOF[A]): F[A] = ioF match {
      case Read => Sync[F].delay(readline)
      case Write(msg) => Sync[F].delay(println(s"$msg"))
    }
  }
}

Combining our effects

And finally, we should need to build everything together. For that purpose, we will need a EitherK. This datatype basically tells the typesystem about our effects, saying that our Eff type can be either a Log or a IOF value.

object App {
  import IOEff._
  import Log._

  type Eff[A] = EitherK[LogF, IOF, A]

  val name = "pepegar"

  def program(implicit Log: LogC[Eff], IO: IOC[Eff]) = for {
    // this dinamically generated documentation, we cannot ask for input, but we should do `name <- IO.read`
    _ <- Log.info(s"name was $name")
    _ <- IO.write(s"hello $name")
  } yield name

  def interp[F[_]: Sync]: Eff ~> F = Log.interp or IOEff.interp
}

You could use this as follows:

scala> import cats.effect.IO
import cats.effect.IO

scala> App.program foldMap App.interp[IO]
res0: cats.effect.IO[String] = IO$779064764

Interleaving Hammock algebras in a Free program

object App {
  import hammock.Uri
  import IOEff._
  import Log._
  import cats._
  import cats.implicits._
  import cats.effect.IO
  import hammock._
  import hammock.marshalling._
  import hammock.apache._

  type Eff1[A] = EitherK[LogF, IOF, A]
  type Eff2[A] = EitherK[HttpF, Eff1, A]
  type Eff[A] = EitherK[MarshallF, Eff2, A]

  implicit val dummyDecoder: Decoder[String] = new Decoder[String] {
    def decode(a: Entity) = a.toString.asRight
  }

  def program(implicit
    Log: LogC[Eff],
    IO: IOC[Eff],
    Hammock: HttpRequestC[Eff],
	Marshall: MarshallC[Eff]
  ) = for {
    _ <- IO.write("What's the ID?")
    id = "4" // for the sake of docs, lets hardcode this... It should be `id <- IO.read`
    _ <- Log.info(s"id was $id")
    response <- Hammock.get(uri"https://jsonplaceholder.typicode.com/users?id=${id.toString}", Map())
	parsed <- Marshall.unmarshall[String](response.entity)
  } yield response

  def interp1[F[_]: Sync]: Eff1 ~> F = Log.interp or IOEff.interp
  def interp2[F[_]: Sync]: Eff2 ~> F = ApacheInterpreter[F].trans or interp1 // interpret HttpF's effects
  def interp[F[_]: Sync]: Eff ~> F = marshallNT[F] or interp2[F] // interpret MarshallF's effects
}

Result

scala> val result = App.program foldMap App.interp[IO]
result: cats.effect.IO[hammock.HttpResponse] = IO$1436445284

scala> result.unsafeRunSync
What's the ID?
[info]: id was 4
res1: hammock.HttpResponse = HttpResponse(Status(200,OK,OK),Map(Vary -> Origin, Accept-Encoding, Expect-CT -> max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct", Transfer-Encoding -> chunked, Server -> cloudflare, Access-Control-Allow-Credentials -> true, Etag -> W/"23f-dNckueF2Qy9kPGGjhoIfeuBW8rg", Expires -> Mon, 11 Feb 2019 20:11:26 GMT, Connection -> keep-alive, CF-Cache-Status -> HIT, Cache-Control -> public, max-age=14400, X-Content-Type-Options -> nosniff, Via -> 1.1 vegur, Content-Type -> application/json; charset=utf-8, Pragma -> no-cache, Date -> Mon, 11 Feb 2019 16:11:26 GMT, CF-RAY -> 4a7817654d7163b5-FRA, Set-Cookie -> __cfduid=d2a5f44c855411f8a00aec26038fd5c631549901486; expires=Tue, 11-Feb-20 16:11:26 GMT; pat...