1

I am using circe to serialize/deserialize some reasonably large models, where each leaf field is a strong type (e.g. case class FirstName(value: String) extends AnyVal).

Implicit resolution/derivation of an Encoder or Decoder is slow.

I have my own codec for which I add some extra Encoder and Decoder instances:

trait JsonCodec extends AutoDerivation {
    // ...
}

With the following method to help with decoding:

package json extends JsonCodec {

  implicit class StringExtensions(val jsonString: String) extends AnyVal {
    def decodeAs[T](implicit decoder: Decoder[T]): T =
      // ...
  }

}

The problem is that every time I call decodeAs, it implicitly derives a Decoder which causes the compilation times to increase massively.

Is there any way I can (generically) cache the implicits such that it will only generate a Decoder once?

Cheetah
  • 13,785
  • 31
  • 106
  • 190
  • I'm not sure if it would be possible in your `StringExtensions` **class**. But, whenever you call your `decodeAs` **method**, you may first do this :`implicit val tDecoder: Decoder[T] = derieveDecoder` _(change `T` with your own type)_. That way, all calls to `decodeAs[T]` it will use the val, instead of derive a new Decoder. _**Note:** If your model consist of many nested types, create decoders for each of them in the reverse order_. – Luis Miguel Mejía Suárez Jan 28 '19 at 22:24
  • I don't think that Luis idea as is saves anything because the compiler still needs to generate all those `Decoder`s for every `implicit val`. The only way I can think of is to put all those decoders for your types as `implicit val`s in some global known static places like your `json` package object itself. Then there can be only one such `implicit val` for each time and the compiler can use those once-derived values every time such `Decoder` is needed (assuming you `import` them into your context). – SergGr Jan 29 '19 at 00:47

1 Answers1

2

Why you can't do this generically

This is not possible, since what you are asking boils down to caching a def. Part of the problem is that producing an implicit instance can (although it rarely does) have side effects. Pathological example:

scala> var myVar: Int = 0
myVar: Int = 0

scala> :paste
// Entering paste mode (ctrl-D to finish)

trait DummyTypeclass[T] { val counter: Int }
implicit def dummyInstance[T]: DummyTypeclass[T] = {
  myVar += 1
  new DummyTypeclass[T] {
    val counter = myVar
  }
}

// Exiting paste mode, now interpreting.

defined trait DummyTypeclass
dummyInstance: [T]=> DummyTypeclass[T]

scala> implicitly[DummyTypeclass[Int]].count
res1: Int = 1

scala> implicitly[DummyTypeclass[Boolean]].counter
res2: Int = 2

scala> implicitly[DummyTypeclass[Int]].counter
res3: Int = 3

As you can see, caching the value of DummyTypeclass[Int] would break its "functionality".

The next best thing

The next best thing is to manually cache instances for a bunch of types. In order to avoid boilerplate, I recommend the cachedImplicit macro from Shapeless. For your decoder example, you end up with:

package json extends JsonCodec {

  import shapeless._

  implicit val strDecoder:  Decoder[String]    = cachedImplicit
  implicit val intDecoder:  Decoder[Int]       = cachedImplicit
  implicit val boolDecoder: Decoder[Boolean]   = cachedImplicit
  implicit val unitDecoder: Decoder[Unit]      = cachedImplicit
  implicit val nameDecoder: Decoder[FirstName] = cachedImplicit
  // ...

  implicit class StringExtensions(val jsonString: String) extends AnyVal {
    // ...
  }

}

If you don't like macros, you can do this manually (basically just what the Shapeless macro does), but it might be less fun. This uses a little known trick that implicits can be "hidden" by shadowing their name.

package json extends JsonCodec {

  implicit val strDecoder:  Decoder[String] = {
    def strDecoder = ???
    implicitly[Decoder[String]]
  }
  implicit val intDecoder:  Decoder[Int] = {
    def intDecoder = ???
    implicitly[Decoder[Int]]
  }
  // ...

}
Alec
  • 31,829
  • 7
  • 67
  • 114