I'm trying to port the Scala track of Exercism to Scala 3. Currently stuck on the "testgen" subproject, as I have next to no experience of reflections and macros. This Scala 2 method seems to ensure that a String can be coerced to a literal constant before stringifying back the result?
It is used in the KinderGardenTestGenerator and WordCountTestGenerator, presumably to sanitize student input?
So I want to replace it in Scala 3 with something like
def escape(raw: String): String = {
Literal(StringConstant(Expr(e))).toString
}
It seems to get access to the reflect methods you need a (using Quotes)
and to do that you need to use inline
.
The closest I've gotten to a solution is this, splitting out the methods into their own object:
import scala.quoted.*
object Escaper {
inline def escape(inline raw: String): String = ${literalize('{raw})}
def literalize(e: Expr[String])(using Quotes): Expr[String] = {
import quotes.reflect.*
Expr(Literal(StringConstant(e.valueOrAbort)).toString)
}
}
It seem to compile, but fails once it reaches compiling KinderGardenTestGenerator, where I get the following error:
[error] -- Error: /home/larsw/projects/scala/testgen/src/main/scala/KindergartenGardenTestGenerator.scala:33:47
[error] 33 | s"""Garden.defaultGarden(${escape(diagram.toString)}).$property("$student")"""
[error] | ^^^^^^^^^^^^^^^^^^^^^^^^
[error] | Could not find class testgen.Escaper$ in classpath
[error] |----------------------------------------------------------------------------
[error] |Inline stack trace
[error] |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[error] |This location contains code that was inlined from Escaper.scala:8
[error] 8 | inline def escape(inline raw: String): String = ${literalize('{raw})}
[error] | ^^^^^^^^^^^^^^^^^^^^^
[error] ----------------------------------------------------------------------------
[error] -- Error: /home/larsw/projects/scala/testgen/src/main/scala/WordCountTestGenerator.scala:25:37
[error] 25 | val sentence = Escaper.escape(args("sentence").toString)
[error] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[error] | Could not find class testgen.Escaper$ in classpath
[error] |----------------------------------------------------------------------------
[error] |Inline stack trace
[error] |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[error] |This location contains code that was inlined from Escaper.scala:8
[error] 8 | inline def escape(inline raw: String): String = ${literalize('{raw})}
[error] | ^^^^^^^^^^^^^^^^^^^^^
[error] ----------------------------------------------------------------------------
And it feels like overkill to inline, my use case isn't that advanced, I don't need to generate code. My questions are:
- Is there a way to Literalize and sanitize the strings without macros or reflection?
- Or is there some way to access the reflection methods without
inline
and(using Quotes)
- Or if my proposed solution is the only way - Why does it not find it on the classpath, even though import seems to work?
(Also interested in links to good videos or courses teaching Scala 3 macros. I'm eager to learn more and is very excited about the possibilities here, especially since Exercism are adding "representers" that can give students and mentors feedback and improvement suggestions for solutions, which looks like a great fit for macros.)
CodePudding user response:
This Scala 2 method seems to ensure that a String can be coerced to a literal constant before stringifying back the result?
No, this is just an exotic way to transform \n
into \\n
etc.
Scala: How can I get an escaped representation of a string?
Your Scala 3 macro now does the wrong job. Try Simão Martins's answer from there
import scala.quoted.*
inline def escape(inline raw: String): String = ${escapeImpl('{raw})}
def escapeImpl(raw: Expr[String])(using Quotes): Expr[String] =
import quotes.reflect.*
Literal(StringConstant(raw.show)).asExprOf[String]
I guess a Scala 3 macro is now an overkill. Just try to escape with a different implemenation from there, e.g. 0__'s
def escape (s: String): String = "\"" escape0(s) "\""
def escape0(s: String): String = s.flatMap(escapedChar)
def escapedChar(ch: Char): String = ch match {
case '\b' => "\\b"
case '\t' => "\\t"
case '\n' => "\\n"
case '\f' => "\\f"
case '\r' => "\\r"
case '"' => "\\\""
case '\'' => "\\\'"
case '\\' => "\\\\"
case _ => if (ch.isControl) "\\0" Integer.toOctalString(ch.toInt)
else String.valueOf(ch)
}