Worked Examples

First, some value classes we’ll be using in all the examples:

@Atomic case class Username(value: String)
@Atomic case class UserId(value: Int)
@Atomic case class Gender(value: String)
@Atomic case class FavouriteAnimal(value: String)

Example 1: Boilerplate reduction

You probably have a case class representing a database table, and a different class to return it in the API.

Some times (or most of the time), these are probably very similar or identical. But they need to be separate so you don’t automatically reveal new database information. Or because you don’t want to reveal sensitive information such as the user’s internal id.

So imagine the following:

case class DbUser(
  id: UserId,
  name: Username,
  gender: Gender,
  favouriteAnimal: FavouriteAnimal
)

case class ApiUser(
  name: Username,
  animal: FavouriteAnimal,
  gender: Gender
)

Now, to turn one into the other you would usually have to write:

def dbToApi(u: DbUser): ApiUser = ApiUser(
  name = u.name,
  animal = u.favouriteAnimal,
  gender = u.gender
)

At no point above did we make a choice: that code could write itself. And, indeed, it does! With a Mixer:

def dbToApi2(u: DbUser): ApiUser = Mixer[DbUser, ApiUser].mix(u)

Database and API classes can get pretty big, and intertwined. Alphabet soup cuts out a lot of repetitive boilerplate.

Example 2: ApplicativeAsk

Suppose you have the following ApplicativeAsk instance, for some functor F[_]:

val aa: ApplicativeAsk[F, (A, B, C, D)] = null

and you wanted to read (B, D). You would have to do the following:

val result: F[(B, D)] = aa.ask.map(tuple => tuple._2 -> tuple._4)

It’s even worse if the tuple is larger, or nested, or contains case classes to project as well.

You might be in a context where A and C don’t even make sense, so you want to pass in an ApplicativeAsk[F, (B, D)] instead, forgetting about A and C. This would be several lines of canonical code.

Luckily, alphabet soup can reduce this to nothing at the call-site:

import shapeless.=:!=

implicit def getAA[M[_], X, Y](
  implicit ax: ApplicativeAsk[M, X],
  ev: X =:!= Y,
  m: Mixer[X, Y]
): ApplicativeAsk[M, Y] = new ApplicativeAsk[M, Y] {
  val applicative = ax.applicative
  def ask: M[Y] = applicative.map(ax.ask)(m.mix)
  def reader[Z](f: Y => Z): M[Z] = ax.reader[Z](a => f(m.mix(a)))
}

Now, we have our original ApplicativeAsk:

implicit val applicativeAsk: ApplicativeAsk[F, (A, B, C, D)] = null

and we can get any sub-ApplicativeAsk, for free:

implicitly[ApplicativeAsk[F, (B, D)]]
implicitly[ApplicativeAsk[F, (B, A)]]
implicitly[ApplicativeAsk[F, (C, D, B)]]
implicitly[ApplicativeAsk[F, B]]

Example 3: MonadState

This is the same problem as ApplicativeAsk. You might have the following MonadState instance for some monad F[_]:

val ms: MonadState[F, (A, B, C, D)] = null

Suppose you had a function f: (B, D) => (B, D), to mutate the B and D values in that tuple. What would you have to do?

ms.modify { case (a, b, c, d) =>
  val (b2, d2) = f(b, d)
  (a, b2, c, d2)
}

Awful! What if it was a case class, or nested, or even larger? As with above that code has no choices in it, it is canonical. Alphabet soup can write that code for you:

implicit def projectMonadState[M[_], X, Y](
  implicit ms: MonadState[M, X],
  ev: X =:!= Y,
  m: Mixer[X, Y]
): MonadState[M, Y] = new MonadState[M, Y] {
  val monad: Monad[M] = ms.monad

  def get: M[Y] = monad.map(ms.get)(m.mix)

  def set(y: Y): M[Unit] = ms.modify(x => m.inject(y, x))

  def inspect[T](f: Y => T): M[T] = ms.inspect(x => f(m.mix(x)))

  def modify(f: Y => Y): M[Unit] = ms.modify(x => m.modify(f)(x))
}

Here’s a quick rundown of what’s happening:

The arguments

The implicit arguments are exactly the same as the ApplicativeAsk example, for the same reasons

get

This is the same as the ApplicativeAsk case. You get the X value and mix it to Y.

set

Here we see the first use of Mixer[X, Y].inject(y, x).

What this does is replace all Y-atoms in x, with the values provided by y.

It is equivalent to Mixer[(X, Y), Y].mix, and is provided for you to avoid the extra compile time calculating that extra Mixer.

In our above problem example using Y = (B, D) and X = (A, B, C, D), we replace the B and D values in X with the values from Y.

inspect

This one is again fairly obvious, and we combine the types the only way we can. We project X to Y, and then apply the provided f.

modify

Here we use Mixer[X, Y].modify(f)(x), for some f: Y => Y.

What modify does is lift a function f: Y => Y to the larger context X => X. That is, it provides a function X => X which has the effect of mutating the Y-atoms within X according to f.

In our above example with Y = (B, D) and X = (A, B, C, D), our f: (B, D) => (B, D) would be lifted to:

val g: (A, B, C, D) => (A, B, C, D) = { case (a, b, c, d) =>
  val (b2, d2) = f(b, d)
  (a, b2, c, d2)
}

Now, taking our original MonadState:

implicit val monadState: MonadState[F, (A, B, C, D)] = null

we can generate any sub-MonadState, for free:

implicitly[MonadState[F, (B, D)]]
implicitly[MonadState[F, (B, A)]]
implicitly[MonadState[F, (C, D, B)]]
implicitly[MonadState[F, B]]