RobertJo
RobertJo

Reputation: 125

Using upickle read in Scala 3 macro

Try to write a generic macro for deserialising case classes using uPickle read in Scala 3:

inline def parseJson[T:Type](x: Expr[String])(using Quotes): Either[String, Expr[T]] = '{
  try 
    Right(read[T]($x.toString))
  catch  
    case e: Throwable => Left(s"Deserialization error: ${e.getMessage}") 
}

get error:

  Right(read[T]($x.toString))
        ^^^^^^^^^^^^^^^^^^^^
missing argument for parameter evidence$3 of method read in trait Api: (implicit evidence$3: upickle.default.Reader[T]): T

I use the uPickle read for a bunch of case classes, is there a solution with less boilerplate code? Thanks for any help!

Upvotes: 0

Views: 117

Answers (1)

Mateusz Kubuszok
Mateusz Kubuszok

Reputation: 27595

You need to:

  1. resolve the implicit and place the found value in Expr - you can expand a macro inside an inline def (just like vampyre macros from Scala 2), but NOT when you are constructing expressions with '{}, then you have to have all implicits resolved
  2. return Expr[Either[Throwable, T]] - when creating a body of a Scala 3 macro, the whole returned value has to be an Expr, otherwise you cannot unquote it within ${} (it might work as a value returned from an helper which would turn it into Expr, but ${} accepts only single call to something defined in top level scope)
  3. separate the body of Expr => Expr macro definition from its expansion - you cannot write "just" inline def something[T: Type](a: Expr[T])(using Quotes): Expr[T] = ... - body of a macro is a (non-inline) def taking Types and Exprs and Quotes and returning a single Expr. inline def has to unquote a single Expr with ${}... or just be a normal def which would be copy-pasted into the call site and resolved there. But then it unquotes no Exprs

So it's either:

// Quotes API requires separate inline def sth = ${ sthImpl }
// and def sthImpl[T: Type](a: Expr[...])(using Quotes): Expr[...]

inline def parseJson[T](x: String)(using r: Reader[T]): Either[Throwable, T] =
  ${ parseJsonImpl[T]('x)('r) }

// Since it builds a complete TASTy, it cannot "defer" implicit
// resolution to callsite. It either already gets value to put
// into using, or has to import quotes.*, quotes.reflect.* and
// then use Expr.summon[T].getOrElse(...)

def parseJsonImpl[T:Type](
  x: Expr[String]
)(
  reader: Expr[Reader[T]]
)(using Quotes): Expr[Either[String, T]] = '{
  try 
    Right(read[T]($x)(using $reader))
  catch  
    case e: Throwable => Left(s"Deserialization error: ${e.getMessage}") 
}

or

// inline def NOT using quotation API - NO Quotes, NO '{}

inline def parseJson[T: Reader](x: String): Either[String, T] = {
  try 
    // Here, you can use something like a "vampyre macros" and 
    // let the compiler expand macros at the call site
    Right(read[T](x))
  catch  
    case e: Throwable => Left(s"Deserialization error: ${e.getMessage}") 
}

Upvotes: 2

Related Questions