Getting rid of implicit parameters

In Scala implicit parameters are everywhere, starting from the infamous ExecutionContext that is needed to execute any Future.

However as soon as you require 2 implicit instances of the same type in a function, you compilation fails and you have to make one of them non-implicit. And it works, you can still pass the parameter explicitly where it’s needed but you miss the point of not having to add extra parameter to the function call.

Let me walk you through an example and to make it something useful, let’s try to fix the useless stack traces that one might get when using async code.

Unhelpful stack traces

Ever seen something like this in your traces when you try to debug an issue?

java.lang.OutOfMemoryError: Java heap space
	at com.fasterxml.jackson.core.util.ByteArrayBuilder.toByteArray(ByteArrayBuilder.java:156)
	at com.fasterxml.jackson.databind.ObjectMapper.writeValueAsBytes(ObjectMapper.java:3693)
	at play.api.libs.json.jackson.JacksonJson$.jsValueToBytes(JacksonJson.scala:328)
	at play.api.libs.json.StaticBinding$.toBytes(StaticBinding.scala:29)
	at play.api.libs.json.Json$.toBytes(Json.scala:182)
	at play.api.http.DefaultWriteables.$anonfun$writeableOf_JsValue$1(Writeable.scala:108)
	at play.api.http.DefaultWriteables$$Lambda$1742/0x0000000840c7b840.apply(Unknown Source)
	at play.api.http.Writeable.toEntity(Writeable.scala:24)
	at play.api.mvc.Results$Status.apply(Results.scala:519)
	at... (Unknown Source)
	at scala.util.Success.$anonfun$map$1(Try.scala:255)
	at scala.util.Success.map(Try.scala:213)
	at scala.concurrent.Future.$anonfun$map$1(Future.scala:292)
	at scala.concurrent.Future$$Lambda$1419/0x0000000840b23840.apply(Unknown Source)
	at ...

Not very useful, isn’t it? Almost nothing in that trace points to your own code!

Creating our own stack traces

Let’s see if we can come up with something better. For that we first need something to keep track of our own call stack.

final case class Context(
  stack: List[String]
) {
  def add(name: String): Context =
    copy(stack = name :: stack)
}

object Context {
  val root: Context = Context(Nil)
}

So we have a Context class holding a stack which is simply our collection of calls. It exposes an add method that we can call to add another operation onto our call stack.

Very well! However it will be even nicer if we have a function that we can use in our code to build our call stack:

def ops[A](name: String)(f: => Future[A])(implicit c: Context, ec: ExecutionContext): Future[A] = {
  val context = c.add(name)
  Future.fromTry(Try(f)).flatten.andThen {
    case s: Success[A] => S
    case f @ Failure(e) =>
      System.out.println(s"$name failed with ${e.getMessage}\n${context.stack.mkString("\n")}")
      f
  }
}

Nice! That’s going to print us a nice (rudimentary) and more useful stack trace whenever our application logic block f fails.

This can be used this way in our application code

def greet(name: String)(implicit c: Context, ec: ExecutionContext): Future[String] = {
  ops("greet") {
    Future { s"Hello $name!" }
  }
}

Looks good but that’s a very constrained example. So let’s see what happens with a slightly more complex function. Let’s try to make another call before returning a value

def greeting(time: Instant)(implicit c: Context, ec: ExecutionContext): Future[String] =
  ops("greeting") {
    Future {
      if (time.isBefore(time.truncatedTo(DAYS).plus(1, HALF_DAYS))) "Good morning"
      else "Hello"
    }
  }

def greet(name: String)(implicit c: Context, ec: ExecutionContext): Future[String] = {
  ops("greet") {
    greeting().map { hello =>
      s"$hello $name"
    }
  }
}

Now we do have a problem because the context we’re passing to the greeting function is the initial (empty) one, that same we passed implicitly to the greet function which doesn’t contain greet in its stack.

So to fix that greet should give us back the new context, so that we can pass it to the nested calls. Let’s try that

def ops[A](name: String)(f: Context => Future[A])(implicit c: Context, ec: ExecutionContext): Future[A] = {
  val context = c.add(name)
  Future.fromTry(Try(f(context))).flatten.andThen {
    case s: Success[A] => s
    case f @ Failure(e) =>
      System.out.println(s"$name failed with ${e.getMessage}\n${context.stack.mkString("\n")}")
      f
  }
}

When we’re making nested calls we’re dealing with the f code block, so we’ve changed it to be a function taking a Context as a parameter and you can see that we’re passing our new context to this f function so that it contains the current operation in its stack.

Ideally we’d like our code to become something like

def greet(name: String)(implicit c: Context, ec: ExecutionContext): Future[String] = {
  ops("greet") { implicit ctx =>
    greeting().map { hello =>
      s"$hello $name"
    }
  }
}

Note the new implicit ctx here, that would be ideal, right? Inside our nested scope we use the new ctx and in the outer scope we use the original one passed to the greet function.

However scala doesn’t quite work this way, as it can’t decide bwtween the 2 implicit instances, and we have to resort to pass the right context explicitly like so

def greet(name: String)(implicit c: Context, ec: ExecutionContext): Future[String] = {
  ops("greet") { ctx =>
    greeting()(ctx, ec).map { hello =>
      s"$hello $name"
    }
  }
}

It works but it’s not very nice, as we have to pass all the implicit parameters explicitly including the ExecutionContext for our Future.

So the question is can we do any better? … and the answer is yes. Hooray!

For that we need to review what we’ve done so far. The clue lies in the way we pass the new Context to our nested call. We’ve just turned our Future[A] into a function Context => Future[A].

What if we replace all our effect type to be a function – as we already tried previously with our lazy future?

Then all our Future[A] become Context => Future[A] which is quite tedious to write all over the place so let’s use a type alias for that

type F[A] = Context => Future[A]

Oh no we’ve just lost our very essential map and flatMap operations! So how do make this effect composable again.

We can provide all these functions in an extension class for our effect type but, fortunately for us, composing funtions is a very common problem that has been solved in the form of a Kleisli.

Don’t fear the name. In essence a Kleisli is just a wrapper around a function that provide composable methods like map and flatMap. And that comes as standard from the cats library.

So let’s use just that instead, so our type alias becomes

type F[A] = Kleisli[Future, Context, A]

and our ops function becomes

def ops[A](name: String)(f: F[A])(implicit ec: ExecutionContext): F[A] = Kleisli { c =>
  val context = c.add(name)
  Future.fromTry(Try(f.run(context))).flatten.andThen {
    case s: Success[A] => s
    case f @ Failure(e) =>
      System.out.println(s"$name failed with ${e.getMessage}\n${context.stack.mkString("\n")}")
      f
  }
}

So what have we done here? Not much, we’ve use our effect type both for the f params and our return type. We’ve wrapped everything in a Kleisli but in the end of the day it’s just a function that takes a Context (c) as input and we need to call run on our Kleisli (f) to execute its function.

However we still have this pesky implicit ExecutionContext but we’ll come back to that later.

Let’s have a look at our client code now

def greeting(time: Instant)(implicit ec: ExecutionContext): F[String] =
  ops("greeting") {
    Kleisli.pure {
      if (time.isBefore(time.truncatedTo(DAYS).plus(1, HALF_DAYS))) "Good morning"
      else "Hello"
    }
  }

def greet(name: String)(implicit ec: ExecutionContext): F[String] = {
  ops("greet") {
    greeting().map { hello =>
      s"$hello $name"
    }
  }
}

No big changes here. Our greeting function uses Kleisli.pure to construct a Kleisli out of a pure value and our greet function has become much nicer as we no longer need to worry about the nested context. Our ops makes sure that the nested call is using the new context. Sweet!

How do we run the whole thing? Simple we need to run the resulting Kleisli with an initial context:

val app = greeting("Bob")
val future = app.run(Context.root)

Not bad but let’s try to get rid of our implicit ExecutionContext. How? Simple let’s just move it into our own Context

final case class Context(
  stack: List[String],
  ec: ExecutionContect
) {
  def add(name: String): Context =
    copy(stack = name :: stack)
}

object Context {
  def root(ec: ExecutionContext): Context = Context(Nil, ec)
}

Now we’re ready to get rid of the ExecutionContext everywhere in our code, starting with our ops function

def ops[A](name: String)(f: F[A]): F[A] = Kleisli { c =>
  val context = c.add(name)
  Future.fromTry(Try(f.run(context))).flatten.andThen {
    case s: Success[A] => s
    case f @ Failure(e) =>
      System.out.println(s"$name failed with ${e.getMessage}\n${context.stack.mkString("\n")}")
      f
  }(context.ec)
}

ok we sill need to now pass it explicitly here but it’s ok as we only have to do it here and not in our application code which becomes much nicer

def greeting(time: Instant): F[String] =
  ops("greeting") {
    Kleisli.pure {
      if (time.isBefore(time.truncatedTo(DAYS).plus(1, HALF_DAYS))) "Good morning"
      else "Hello"
    }
  }

def greet(name: String): F[String] = {
  ops("greet") {
    greeting().map { hello =>
      s"$hello $name"
    }
  }
}

Almost perfect, isn’t it? And what’s more? Well we can extend that to a lot of places …

You don’t like the println, do you? Rather use a Logger instead? Easy stick one into your Context

final case class Context(
  stack: List[String],
  ec: ExecutionContect,
  logger: Logger
) {
  def add(name: String): Context =
    copy(stack = name :: stack)
}

object Context {
  def root(logger: Logger, ec: ExecutionContext): Context = Context(Nil, ec)
}

and use it in our ops function

def ops[A](name: String)(f: F[A]): F[A] = Kleisli { c =>
  val context = c.add(name)
  Future.fromTry(Try(f.run(context))).flatten.andThen {
    case s: Success[A] => s
    case f @ Failure(e) =>
      logger.error(s"$name failed on [${context.stack.mkString(", ")}]", e)
      f
  }(context.ec)
}

And we can do so much more, we can

  • track down execution times
  • gather logs so that you can decide to log them (e.g in case of errors) or drop them (e.g. in case of success)…

Got a few HTTP headers that you need to propagate to the downstream services? Easy stick that in your context too and add an extension method to your HTTP request/client to automatically add those headers.

Assuming you’re using Play it something like this

final case class Context(
  stack: List[String],
  ec: ExecutionContext,
  req: RequestHeader,
  logger: Logger) {
  def shift(ec: ExecutionContext): Context = copy(ec = ec)
  def add(name: String): Context = copy(stack = name :: stack)
  def propagatedHeaders: Seq[(String, String)] = req.headers.headers.filter {
    case (name, _) => Context.propagatedHeaders.contains(name.toLowerCase)
  }
}

object Context {
  def root(ec: ExecutionContext, req: RequestHeader, logger: Logger): Context =
    Context(Nil, ec, req, logger)
  val propagatedHeaders: Set[String] = Set("x-trace-id", "x-span-id")
}

and on the http client side we make the header propagation available to anyone with an implicit class

implicit final class RequestOps(val req: WSRequest) extends AnyVal {
    def send(): F[WSResponse] = Kleisli { ctx =>
      req.addHttpHeaders(ctx.propagatedHeaders: _*)
      req.execute()
    }
  }

so when using an http client, everything is simple as

val client: WSClient = ??? // create your client instance here
val response: F[WSResponse] = 
  client
    .url(endpoint)
    .withMethod(GET)
    .withHttpHeaders(authorization)
    .withRequestTimeout(timeout)
    .send() // gives back a F[_] and automatically propagate the x-trace/span-id headers

I find this approach very flexible as you can tune it to your very needs: you can pick the effect type you like (no problem using IO instead of Future) and you can make many features available “for free” into your application code.