scalascala-macrosscala-3inlininggeneric-derivation

How to access method valueOf of an unknown enum in Scala 3


I'm trying to create inline def to generate Json codec for any enums in Scala 3. For this I need to have access to valueOf method of the parent of the enum. Something like this:

inline def gen[T](using JsonCodec[String], T <:< reflect.Enum): JsonCodec[T] = ???

How this can be achieved?

After reading comments now I changed my code to:

    val decoder: JsonDecoder[T] = JsonDecoder[String].mapOrFail(v => Try(${getEnum[T]}(v)).fold(e => Left(e.getMessage), v => Right(v)))
    val encoder: JsonEncoder[T] = JsonEncoder[String].contramap(_.toString)
    JsonCodec.apply(encoder, decoder)

  def getEnum[T: Type](using Quotes): Expr[String => T] =
    import quotes.reflect.*
    val companion = Ref(TypeTree.of[T].symbol.companionModule)
    Select.unique(companion, "valueOf").asExprOf[String => T]

And compiler complains with:


Malformed macro.

Expected the splice ${...} to be at the top of the RHS:
  inline def foo(inline x: X, ..., y: Y): Int = ${ impl('x, ... 'y) }

 * The contents of the splice must call a static method
 * All arguments must be quoted

Solution

  • As a json library you seem to use https://zio.github.io/zio-json

    Malformed macro.

    Expected the splice ${...} to be at the top of the RHS

    I guess the error is understandable. You're not supposed to use macro implementations directly like that.

    Either you have a macro (inline method) getEnum and its implementation getEnumImpl (returning an Expr) and you use getEnum (not getEnumImpl)

    import zio.json.{JsonDecoder, JsonEncoder, JsonCodec}
    import scala.quoted.*
    
    inline def gen[T]: JsonCodec[T] =
      val decoder: JsonDecoder[T] = JsonDecoder[String].mapOrFail(v => util.Try(getEnum[T](v)).fold(e => Left(e.getMessage), v => Right(v)))
      val encoder: JsonEncoder[T] = JsonEncoder[String].contramap(_.toString)
      JsonCodec.apply(encoder, decoder)
    
    inline def getEnum[T]: String => T = ${ getEnumImpl[T] }
    
    def getEnumImpl[T: Type](using Quotes): Expr[String => T] =
      import quotes.reflect.*
    
      val companion = Ref(TypeTree.of[T].symbol.companionModule)
      Select.unique(companion, "valueOf").asExprOf[String => T]
    

    or getEnum is a macro implementation itself and you use it in another macro implementation

    import zio.json.{JsonDecoder, JsonEncoder, JsonCodec}
    import scala.quoted.*
    
    inline def gen[T]: JsonCodec[T] = ${genImpl[T]}
    
    def genImpl[T: Type](using Quotes): Expr[JsonCodec[T]] =
      '{
        val decoder: JsonDecoder[T] = JsonDecoder[String].mapOrFail(v => util.Try(${ getEnum[T] }(v)).fold(e => Left(e.getMessage), v => Right(v)) )
        val encoder: JsonEncoder[T] = JsonEncoder[String].contramap(_.toString)
        JsonCodec.apply(encoder, decoder)
      }
    
    def getEnum[T: Type](using Quotes): Expr[String => T] =
      import quotes.reflect.*
    
      val companion = Ref(TypeTree.of[T].symbol.companionModule)
      Select.unique(companion, "valueOf").asExprOf[String => T]
    

    Now the error is

    enum Color:
      case Red, Green, Blue
    
    gen[Color].encodeJson(Color.Red, None)
    
    // Exception occurred while executing macro expansion.
    // java.lang.Exception: Expected an expression.
    // This is a partially applied Term. Try eta-expanding the term first.
    

    You can wrap Select.unique with Apply (and replace functions String => T with methods). So either

    inline def gen[T <: reflect.Enum]: JsonCodec[T] =
      val decoder: JsonDecoder[T] = JsonDecoder[String].mapOrFail(v => util.Try(getEnum[T](v)).fold(e => Left(e.getMessage), v => Right(v)))
      val encoder: JsonEncoder[T] = JsonEncoder[String].contramap(_.toString)
      JsonCodec.apply(encoder, decoder)
    
    inline def getEnum[T](v: String): T = ${ getEnumImpl[T]('v) }
    
    def getEnumImpl[T: Type](v: Expr[String])(using Quotes): Expr[T] =
      import quotes.reflect.*
    
      val companion = Ref(TypeTree.of[T].symbol.companionModule)
      Apply(Select.unique(companion, "valueOf"), List(v.asTerm)).asExprOf[T]
    

    or

    inline def gen[T <: reflect.Enum]: JsonCodec[T] = ${genImpl[T]}
    
    def genImpl[T: Type](using Quotes): Expr[JsonCodec[T]] =
      '{
        val decoder: JsonDecoder[T] = JsonDecoder[String].mapOrFail(v => util.Try(${ getEnum[T]('v) }).fold(e => Left(e.getMessage), v => Right(v)) )
        val encoder: JsonEncoder[T] = JsonEncoder[String].contramap(_.toString)
        JsonCodec.apply(encoder, decoder)
      }
    
    def getEnum[T: Type](v: Expr[String])(using Quotes): Expr[T] =
      import quotes.reflect.*
    
      val companion = Ref(TypeTree.of[T].symbol.companionModule)
      Apply(Select.unique(companion, "valueOf"), List(v.asTerm)).asExprOf[T]
    

    Also in order to derive JsonCodec for enums you can use Mirror

    import scala.compiletime.{constValue, erasedValue, summonInline}
    import scala.deriving.Mirror
    
    inline given [T <: reflect.Enum with Singleton](using
      m: Mirror.ProductOf[T]
    ): JsonCodec[T] =
      val decoder: JsonDecoder[T] =
        val label = constValue[m.MirroredLabel]
        JsonDecoder[String].mapOrFail(v => Either.cond(label == v, m.fromProduct(EmptyTuple), s"$label != $v"))
      val encoder: JsonEncoder[T] = JsonEncoder[String].contramap(_.toString)
      JsonCodec.apply(encoder, decoder)
    
    inline def mkSumDecoder[T, Tup <: Tuple]: JsonDecoder[T] = inline erasedValue[Tup] match
      case _: (h *: EmptyTuple) => summonInline[JsonDecoder[h & T]].widen[T]
      case _: (h *: t) => summonInline[JsonDecoder[h & T]].widen[T] <> mkSumDecoder[T, t]
    
    inline given [T <: reflect.Enum](using
      m: Mirror.SumOf[T]
    ): JsonCodec[T] =
      val decoder: JsonDecoder[T] = mkSumDecoder[T, m.MirroredElemTypes]
      val encoder: JsonEncoder[T] = JsonEncoder[String].contramap(_.toString)
      JsonCodec.apply(encoder, decoder)
    

    Testing:

    import zio.json.given
    
    (Color.Blue: Color.Blue.type).toJson // "Blue"
    (Color.Blue: Color).toJson           // "Blue"
    """ "Blue" """.fromJson[Color.Blue.type] // Right(Blue)
    """ "Blue" """.fromJson[Color]           // Right(Blue)
    

    There can be also cases depending on a parameter like in

    enum Color:
      case Red(i: Int)
      case Green, Blue
    

    We can also use https://github.com/typelevel/shapeless-3 for derivation or derive in Shapeless 2 style

    sealed trait Coproduct extends Product with Serializable
    sealed trait +:[+H, +T <: Coproduct] extends Coproduct
    final case class Inl[+H, +T <: Coproduct](head: H) extends (H +: T)
    final case class Inr[+H, +T <: Coproduct](tail: T) extends (H +: T)
    sealed trait CNil extends Coproduct
    
    object Coproduct:
      def unsafeToCoproduct(length: Int, value: Any): Coproduct =
        (0 until length).foldLeft[Coproduct](Inl(value))((c, _) => Inr(c))
    
      @scala.annotation.tailrec
      def unsafeFromCoproduct(c: Coproduct): Any = c match
        case Inl(h) => h
        case Inr(c) => unsafeFromCoproduct(c)
        case _: CNil => sys.error("impossible")
    
      type ToCoproduct[T <: Tuple] <: Coproduct = T match
        case EmptyTuple => CNil
        case h *: t => h +: ToCoproduct[t]
    
      type ToTuple[C <: Coproduct] <: Tuple = C match
        case CNil => EmptyTuple
        case h +: t => h *: ToTuple[t]
    
    trait Generic[T]:
      type Repr
      def to(t: T): Repr
      def from(r: Repr): T
    
    object Generic:
      type Aux[T, Repr0] = Generic[T] {type Repr = Repr0}
      def instance[T, Repr0](f: T => Repr0, g: Repr0 => T): Aux[T, Repr0] =
        new Generic[T]:
          override type Repr = Repr0
          override def to(t: T): Repr0 = f(t)
          override def from(r: Repr0): T = g(r)
    
      object ops:
        extension[A] (a: A)
          def toRepr(using g: Generic[A]): g.Repr = g.to(a)
    
        extension[Repr] (a: Repr)
          def to[A](using g: Generic.Aux[A, Repr]): A = g.from(a)
    
      given[T <: Product](using
        // ev: NotGiven[T <:< Tuple],
        // ev1: NotGiven[T <:< Coproduct],
        m: Mirror.ProductOf[T],
        m1: Mirror.ProductOf[m.MirroredElemTypes]
      ): Aux[T, m.MirroredElemTypes] = instance(
        m1.fromProduct,
        m.fromProduct
      )
    
      given[T, C <: Coproduct](using
        // ev: NotGiven[T <:< Tuple],
        // ev1: NotGiven[T <:< Coproduct],
        m: Mirror.SumOf[T],
        ev2: Coproduct.ToCoproduct[m.MirroredElemTypes] =:= C
      ): Generic.Aux[T, C/*Coproduct.ToCoproduct[m.MirroredElemTypes]*/] = instance(
        t => Coproduct.unsafeToCoproduct(m.ordinal(t), t).asInstanceOf[C],
        Coproduct.unsafeFromCoproduct(_).asInstanceOf[T]
      )
    
    inline given singleton[T <: reflect.Enum with Singleton](using
      m: Mirror.ProductOf[T]
    ): JsonCodec[T] =
      val decoder: JsonDecoder[T] =
        val label = constValue[m.MirroredLabel]
        JsonDecoder[String].mapOrFail(v => Either.cond(label == v, m.fromProduct(EmptyTuple), s"$label != $v"))
      val encoder: JsonEncoder[T] = JsonEncoder[String].contramap(_.toString)
      JsonCodec(encoder, decoder)
    
    inline given [T <: reflect.Enum](using
      m: Mirror.ProductOf[T]
    ): JsonCodec[T] = DeriveJsonCodec.gen[T]
    
    given [T <: reflect.Enum](using
      gen: Generic.Aux[T, _ <: Coproduct],
      codec: JsonCodec[gen.Repr]
    ): JsonCodec[T] = codec.transform(gen.from, gen.to)
    
    given [H, T <: Coproduct](using
      hCodec: JsonCodec[H],
      tCodec: JsonCodec[T]
    ): JsonCodec[H +: T] = (hCodec <+> tCodec).transform({
      case Left(h) => Inl(h)
      case Right(t) => Inr(t)
    }, {
      case Inl(h) => Left(h)
      case Inr(t) => Right(t)
    })
    
    given JsonCodec[CNil] = JsonCodec[String].transform(_ => sys.error("impossible"), _ => sys.error("impossible"))
    
    import zio.json.given
    
    (Color.Blue: Color.Blue.type).toJson // "Blue"
    (Color.Blue: Color).toJson           // "Blue"
    (Color.Red(1): Color.Red).toJson // {"i":1}
    (Color.Red(1): Color).toJson     // {"i":1}
    """ "Blue" """.fromJson[Color.Blue.type] // Right(Blue)
    """ "Blue" """.fromJson[Color]           // Right(Blue)
    """{"i":1}""".fromJson[Color.Red] // Right(Red(1))
    """{"i":1}""".fromJson[Color]     // Right(Red(1))