Is it possible to have a sub-trait inheirit a class parameter from another trait?

113 views Asked by At

I am trying to DRY up my code a little bit. I am using Circe to do some decoding. I have several classes and all of them have the form of:

import io.circe.derivation.deriveDecoder
import io.circe.derivation.renaming.snakeCase
import io.circe.parser.decode
import io.circe.{Decoder, Error}

// Getter[A] just defines some functions for getting the data from an endpoint.
class JSONGetter extends Getter[MyClass] {
    implicit val decoder: Decoder[MyClass] = deriveDecoder[MyClass](io.circle.derivation.renaming.snakeCase)

 // .. other parsing code here
}

I would like to stop repeating myself with the implicit so I set out to make a new trait:

trait JsonDecoding[A] { 
        implicit val decoder: Decoder[A] = deriveDecoder[A](io.circle.derivation.renaming.snakeCase)
}

So that I could shorten my class to:

class JSONGetter extends Getter[MyClass] with JsonDecoding[MyClass] {

 // .. other parsing code here
}

This would be very convenient. However, I get A is not a class when attempting to compile. I think that I cannot do this the way I want to here.

Is there a smart way to do this so I can not repeat myself when defining the implicit decoder that only changes in the class being decoded?

1

There are 1 answers

0
Dmytro Mitin On

You can use automatic derivation

import io.circe.generic.auto._

case class Person(name: String)

instead of semi-automatic derivation

io.circe.generic.semiauto

case class Person(name: String)
object Person {
  implicit val fooDecoder: Person[Foo] = semiauto.deriveDecoder
  implicit val fooEncoder: Person[Foo] = semiauto.deriveEncoder
}

or macro annotation @JsonCodec to simplify semi-automatic derivation

import io.circe.generic.JsonCodec

@JsonCodec case class Person(name: String)

Let's assume that you prefer semi-automatic derivation rather than automatic one.

Extending a trait is an incorrect way

class JSONGetter extends Getter[MyClass] with JsonDecoding[MyClass] {
  // ...
}

The thing is that deriveDecoder is a macro and it's important that the macro is expanded in a proper place. If you extend a trait and put implicit there then the macro is expanded in incorrect place.

You can define your own macro annotation that will add necessary implicit

@jsonDecoding
class JSONGetter extends Getter[MyClass]

import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

@compileTimeOnly("enable macro paradise to expand macro annotations")
class jsonDecoding extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro jsonDecodingMacro.impl
}

object jsonDecodingMacro {
  def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
    import c.universe._
    annottees match {
      case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" :: tail =>
        val tparamNames = tparams.map {
          case q"$mods type $tpname[..$tparams] = $tpt" => tpname
        }
        q"""
          $mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self =>
            ..$stats

            implicit val decoder: _root_.io.circe.Decoder[$tpname[..$tparamNames]] =
              _root_.io.circe.derivation.deriveDecoder[$tpname[..$tparamNames]](_root_.io.circe.derivation.renaming.snakeCase)
          }

          ..$tail
        """
        // or should the implicit be added to companion object?

      case q"$mods object $tname extends { ..$earlydefns } with ..$parents { $self => ..$body }" =>
        // ...
      
      case q"$mods trait $tpname[..$tparams] extends { ..$earlydefns } with ..$parents { $self => ..$stats }" =>
        //...
    }
  }
}

Scala | How can this code be put into a macro annotation?

How to reduce boilerplate code with Scala Macros in Scala 2?

Pass implicit parameter through multiple objects

Scala macro-based annotation reuse

Similarly for Cats type classes you can use Kittens to derive the type classes either atomatically

import cats.derived.auto.functor._

case class Cat[Food](food: Food, foods: List[Food])

or semi-automatically

import cats.derived.semiauto

case class Cat[Food](food: Food, foods: List[Food])
object Cat {
  implicit val fc: Functor[Cat] = semiauto.functor
}

If you prefer semi-automatic derivation then you can use Katnip macro annotations rather than write necessary implicit semiauto.functor for every class

import io.scalaland.catnip.Semi

@Semi(Functor) case class Cat[Food](food: Food, foods: List[Food])