I'm trying to implement an implicit materializer as described here: http://docs.scala-lang.org/overviews/macros/implicits.html
I decided to create a macro that converts a case class from and to a String using quasiquotes for prototyping purposes. For example:
case class User(id: String, name: String)
val foo = User("testid", "foo")
Converting foo to text should result in "testid foo" and vice versa.
Here is the simple trait and its companion object I have created:
trait TextConvertible[T] {
def convertTo(obj: T): String
def convertFrom(text: String): T
}
object TextConvertible {
import language.experimental.macros
import QuasiTest.materializeTextConvertible_impl
implicit def materializeTextConvertible[T]: TextConvertible[T] = macro materializeTextConvertible_impl[T]
}
and here is the macro:
object QuasiTest {
import reflect.macros._
def materializeTextConvertible_impl[T: c.WeakTypeTag](c: Context): c.Expr[TextConvertible[T]] = {
import c.universe._
val tpe = weakTypeOf[T]
val fields = tpe.declarations.collect {
case field if field.isMethod && field.asMethod.isCaseAccessor => field.asMethod.accessed
}
val strConvertTo = fields.map {
field => q"obj.$field"
}.reduce[Tree] {
case (acc, elem) => q"""$acc + " " + $elem"""
}
val strConvertFrom = fields.zipWithIndex map {
case (field, index) => q"splitted($index)"
}
val quasi = q"""
new TextConvertible[$tpe] {
def convertTo(obj: $tpe) = $strConvertTo
def convertFrom(text: String) = {
val splitted = text.split(" ")
new $tpe(..$strConvertFrom)
}
}
"""
c.Expr[TextConvertible[T]](quasi)
}
}
which generates
{
final class $anon extends TextConvertible[User] {
def <init>() = {
super.<init>();
()
};
def convertTo(obj: User) = obj.id.$plus(" ").$plus(obj.name);
def convertFrom(text: String) = {
val splitted = text.split(" ");
new User(splitted(0), splitted(1))
}
};
new $anon()
}
The generated code looks fine, but yet I get the error value id in class User cannot be accessed in User in compilation while trying to use the macro.
I suspect I am using a wrong type for fields. I tried field.asMethod.accessed.name, but it results in def convertTo(obj: User) = obj.id .$plus(" ").$plus(obj.name ); (note the extra spaces after id and name), which naturally results in the error value id is not a member of User.
What am I doing wrong?
Ah, figured it out almost immediately after sending my question.
I changed the lines
val fields = tpe.declarations.collect {
case field if field.isMethod && field.asMethod.isCaseAccessor => field.asMethod.accessed
}
to
val fields = tpe.declarations.collect {
case field if field.isMethod && field.asMethod.isCaseAccessor => field.name
}
which solved the problem.
The field you get with accessed.name has a special suffix attached to it, to avoid naming conflicts.
The special suffix is scala.reflect.api.StandardNames$TermNamesApi.LOCAL_SUFFIX_STRING, which has the value, you guessed it, a space char.
This is quite evil, of course.
Related
My task is to print out type information in Java-like notation (using <, > for type arguments notation). In scala 2 I have this small method using scala.reflect.Manifest as a source for type symbol and it's parameters:
def typeOf[T](implicit manifest: Manifest[T]): String = {
def loop[T0](m: Manifest[T0]): String =
if (m.typeArguments.isEmpty) m.runtimeClass.getSimpleName
else {
val typeArguments = m.typeArguments.map(loop(_)).mkString(",")
raw"""${m.runtimeClass.getSimpleName}<$typeArguments>"""
}
loop(manifest)
}
Unfortunately in Scala 3 Manifests are not available. Is there a Scala 3 native way to rewrite this? I'm open to some inline macro stuff. What I have tried so far is
inline def typeOf[T]: String = ${typeOfImpl}
private def typeOfImpl[T: Type](using Quotes): Expr[String] =
import quotes.reflect.*
val tree = TypeTree.of[T]
tree.show
// ^^ call is parameterized with Printer but AFAIK there's no way
// to provide your own implementation for it. You can to chose
// from predefined ones. So how do I proceed from here?
I know that Scala types can't be all represented as Java types. I aim to cover only simple ones that the original method was able to cover. No wildcards or existentials, only fully resolved types like:
List[String] res: List<String>
List[Option[String]] res: List<Option<String>>
Map[String,Option[Int]] res: Map<String,Option<Int>>
I post this answer even though it's not a definitive solution and there's probably a better way but hopefully it can give you some ideas.
I think a good start is using TypeRepr:
val tpr: TypeRepr = TypeRepr.of[T]
val typeParams: List[TypeRepr] = tpr match {
case a: AppliedType => a.args
case _ => Nil
}
Then with a recursive method you should be able to work something out.
Copied from Inspired from https://github.com/gaeljw/typetrees/blob/main/src/main/scala/io/github/gaeljw/typetrees/TypeTreeTagMacros.scala#L12:
private def getTypeString[T](using Type[T], Quotes): Expr[String] = {
import quotes.reflect._
def getTypeStringRec(tpr: TypeRepr)(using Quotes): Expr[String] = {
tpr.asType match {
case '[t] => getTypeString[t]
}
}
val tpr: TypeRepr = TypeRepr.of[T]
val typeParams: List[TypeRepr] = tpr match {
case a: AppliedType => a.args
case _ => Nil
}
val selfTag: Expr[ClassTag[T]] = getClassTag[T]
val argsStrings: Expr[List[String]] =
Expr.ofList(typeParams.map(getTypeStringRec))
'{ /* Compute something using selfTag and argsStrings */ }
}
private def getClassTag[T](using Type[T], Quotes): Expr[ClassTag[T]] = {
import quotes.reflect._
Expr.summon[ClassTag[T]] match {
case Some(ct) =>
ct
case None =>
report.error(
s"Unable to find a ClassTag for type ${Type.show[T]}",
Position.ofMacroExpansion
)
throw new Exception("Error when applying macro")
}
}
The final working solution I came up with was:
def typeOfImpl[T: Type](using Quotes): Expr[String] = {
import quotes.reflect.*
TypeRepr.of[T] match {
case AppliedType(tpr, args) =>
val typeName = Expr(tpr.show)
val typeArguments = Expr.ofList(args.map {
_.asType match {
case '[t] => typeOfImpl[t]
}
})
'{
val tpeName = ${ typeName }
val typeArgs = ${ typeArguments }
typeArgs.mkString(tpeName + "<", ", ", ">")
}
case tpr: TypeRef => Expr(tpr.show)
case other =>
report.errorAndAbort(s"unsupported type: ${other.show}", Position.ofMacroExpansion)
}
}
I am playing with scalameta and I want to have a generic measurement annotation which sends measurements about how long the method execution took.
I used Qing Wei's cache annotation demo.
https://www.cakesolutions.net/teamblogs/scalameta-tut-cache
It works for non async methods but my attribute doesn't match on methods which return Future due to the ExecutionContext argument list.
My annotation looks like this:
package measurements
import scala.concurrent.Future
import scala.meta._
class measure(name: String) extends scala.annotation.StaticAnnotation {
inline def apply(defn: Any): Any = meta {
defn match {
case defn: Defn.Def => {
this match {
case q"new $_($backendParam)" =>
val body: Term = MeasureMacroImpl.expand(backendParam, defn)
defn.copy(body = body)
case x =>
abort(s"Unrecognized pattern $x")
}
}
case _ =>
abort("This annotation only works on `def`")
}
}
}
object MeasureMacroImpl {
def expand(nameExpr: Term.Arg, annotatedDef: Defn.Def): Term = {
val name: Term.Name = Term.Name(nameExpr.syntax)
annotatedDef match {
case q"..$_ def $methodName[..$tps](..$nonCurriedParams): $rtType = $expr" => {
rtType match {
case f: Future[Any] => q"""
val name = $name
println("before " + name)
val future: ${rtType} = ${expr}
future.map(result => {
println("after " + name)
result
})
"""
case _ => q"""
val name = $name
println("before " + name)
val result: ${rtType} = ${expr}
println("after " + name)
result
"""
}
}
case _ => abort("This annotation only works on `def`")
}
}
}
I use the annotation like this:
#measure("A")
def test(x: String): String = x
#measure("B")
def testMultipleArg(x: Int, y: Int): Int = x + y
I would like to use it with async methods like this:
#measure("C")
def testAsync(x: String)(implicit ec: ExecutionContext) : Future[String] = {
Future(test(x))
}
but I get the following error:
exception during macro expansion:
scala.meta.internal.inline.AbortException: This annotation only works on `def`
I assume the issue is MeasureMacroImpl matching but I am not sure how to match on multiple argument groups. Could you guys help me? Any ideas or sample code would be greatly appreciated. I am pretty new to scala and scala meta so apologies if I asked a trivial question.
You are getting error because MeasureMacroImpl does not match curried parameters.
It's fairly trivial to match curried params, simply use
scala
case q"..$_ def $methodName[..$tps](...$nonCurriedParams): $rtType = $expr"
Notice the ...$nonCurriedParams instead of ..$nonCurriedParams
I'm having some problems with a macro I've written to help me log metrics represented as case class instances to to InfluxDB. I presume I'm having a type erasure problem and that the tyep parameter T is getting lost, but I'm not entirely sure what's going on. (This is also my first exposure to Scala macros.)
import scala.language.experimental.macros
import play.api.libs.json.{JsNumber, JsString, JsObject, JsArray}
abstract class Metric[T] {
def series: String
def jsFields: JsArray = macro MetricsMacros.jsFields[T]
def jsValues: JsArray = macro MetricsMacros.jsValues[T]
}
object Metrics {
case class LoggedMetric(timestamp: Long, series: String, fields: JsArray, values: JsArray)
case object Kick
def log[T](metric: Metric[T]): Unit = {
println(LoggedMetric(
System.currentTimeMillis,
metric.series,
metric.jsFields,
metric.jsValues
))
}
}
And here's an example metric case class:
case class SessionCountMetric(a: Int, b: String) extends Metric[SessionCountMetric] {
val series = "sessioncount"
}
Here's what happens when I try to log it:
scala> val m = SessionCountMetric(1, "a")
m: com.confabulous.deva.SessionCountMetric = SessionCountMetric(1,a)
scala> Metrics.log(m)
LoggedMetric(1411450638296,sessioncount,[],[])
Even though the macro itself seems to work fine:
scala> m.jsFields
res1: play.api.libs.json.JsArray = ["a","b"]
scala> m.jsValues
res2: play.api.libs.json.JsArray = [1,"a"]
Here's the actual macro itself:
import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context
object MetricsMacros {
private def fieldNames[T: c.WeakTypeTag](c: Context)= {
val tpe = c.weakTypeOf[T]
tpe.decls.collect {
case field if field.isMethod && field.asMethod.isCaseAccessor => field.asTerm.name
}
}
def jsFields[T: c.WeakTypeTag](c: Context) = {
import c.universe._
val names = fieldNames[T](c)
Apply(
q"play.api.libs.json.Json.arr",
names.map(name => Literal(Constant(name.toString))).toList
)
}
def jsValues[T: c.WeakTypeTag](c: Context) = {
import c.universe._
val names = fieldNames[T](c)
Apply(
q"play.api.libs.json.Json.arr",
names.map(name => q"${c.prefix.tree}.$name").toList
)
}
}
Update
I tried Eugene's second suggestion like this:
abstract class Metric[T] {
def series: String
}
trait MetricSerializer[T] {
def fields: Seq[String]
def values(metric: T): Seq[Any]
}
object MetricSerializer {
implicit def materializeSerializer[T]: MetricSerializer[T] = macro MetricsMacros.materializeSerializer[T]
}
object Metrics {
def log[T: MetricSerializer](metric: T): Unit = {
val serializer = implicitly[MetricSerializer[T]]
println(serializer.fields)
println(serializer.values(metric))
}
}
with the macro now looking like this:
object MetricsMacros {
def materializeSerializer[T: c.WeakTypeTag](c: Context) = {
import c.universe._
val tpe = c.weakTypeOf[T]
val names = tpe.decls.collect {
case field if field.isMethod && field.asMethod.isCaseAccessor => field.asTerm.name
}
val fields = Apply(
q"Seq",
names.map(name => Literal(Constant(name.toString))).toList
)
val values = Apply(
q"Seq",
names.map(name => q"metric.$name").toList
)
q"""
new MetricSerializer[$tpe] {
def fields = $fields
def values(metric: Metric[$tpe]) = $values
}
"""
}
}
However, when I call Metrics.log -- specifically when it calls implicitly[MetricSerializer[T]] I get the following error:
error: value a is not a member of com.confabulous.deva.Metric[com.confabulous.deva.SessionCountMetric]
Why is it trying to use Metric[com.confabulous.deva.SessionCountMetric] instead of SessionCountMetric?
Conclusion
Fixed it.
def values(metric: Metric[$tpe]) = $values
should have been
def values(metric: $tpe) = $values
You're in a situation that's very close to one described in a recent question: scala macros: defer type inference.
As things stand right now, you'll have to turn log into a macro. An alternative would also to turn Metric.jsFields and Metric.jsValues into JsFieldable and JsValuable type classes materialized by implicit macros at callsites of log (http://docs.scala-lang.org/overviews/macros/implicits.html).
I have the following method:
def save(entity: A): Either[List[Error],A] + {....
which I want to test using specs2
I want to test for the existence of a specific error when a required field is not specified, like this:
val noNickname = User(
nickname = "",
name = "new name",
)
noNickname.save must beLeft.like {
case errors => {
atLeastOnceWhen(errors) {
case error => {
error.errorCode must equalTo(Error.REQUIRED)
error.field must equalTo("nickname")
}
}
}
}
It works fine, but I'd like to define my own matcher to make it less verbose, like this:
noNickname.save must haveError.like {
case error => {
error.errorCode must equalTo(Error.REQUIRED)
error.field must equalTo("nickname")
}
}
}
I had a look at the documentation (http://etorreborre.github.com/specs2/guide/org.specs2.guide.Matchers.html#Matchers) but I can't figure out how to define a custom matcher like haveError.like
Here your code with a few changes to make it compile:
case class Error(errorCode: String, field: String)
def save[A](entity: A): Either[List[Error],A] = Left(List(Error("REQUIRED", "nickname")))
case class User(nickname: String, name: String)
val noNickname = User(nickname = "", name = "new name")
"save noNickName" >> {
save(noNickname) must haveError.like {
case error => {
error.errorCode must equalTo("REQUIRED")
error.field must equalTo("nickname")
}
}
}
def haveError[T] = new ErrorMatcher[T]
class ErrorMatcher[T] extends Matcher[Either[List[T], _]] {
def apply[S <: Either[List[T], _]](value: Expectable[S]) =
result(value.value.left.toOption.isDefined,
value.description + " is Left",
value.description + " is not Left",
value)
def like[U](f: PartialFunction[T, MatchResult[U]]) =
this and partialMatcher(f)
private def partialMatcher[U](f: PartialFunction[T, MatchResult[U]]) =
new Matcher[Either[List[T], _]] {
def apply[S <: Either[List[T], _]](value: Expectable[S]) = {
// get should always work here because it comes after the "and"
val errors = value.value.left.toOption.get
val res = atLeastOnceWhen[T, U](errors)(f)
result(res.isSuccess,
value.description+" is Left[T] and "+res.message,
value.description+" is Left[T] but "+res.message,
value)
}
}
}
Notice that the matcher is defined on Either[List[T], _] everywhere.
I'm also wondering about the failure messages that are returned in case you don't find the expected error message, they might not be very explicit when the partial function fails.
So you may want to aim for using a contain matcher. Like this:
"save noNickName" >> {
save(noNickname) must haveError.containing(Error("REQUIRED", "nickname"))
}
// I'm reusing the beLeft matcher here
def haveError[T]: Matcher[Either[List[T], _]] = beLeft
// and using an implicit conversion to extend it
implicit def toErrorListMatcher[T](m: Matcher[Either[List[T], _]]): ErrorListMatcher[T] =
new ErrorListMatcher[T](m)
class ErrorListMatcher[T](m: Matcher[Either[List[T], _]]) {
def containing(t: T) =
// the 'contain' matcher is adapted to take in an
// Either[List[T], _] and work on its left part
m and contain(t) ^^ ((e: Either[List[T], _]) => e.left.toOption.get)
}
[Update]
The first solution (using atLeastOnceWhen and a partial function) can be combined with the second one (using an implicit) and the beLike matcher, to get maximum reusability of existing specs2 code:
def haveError[T]: Matcher[Either[List[T], _] = beLeft
implicit def toErrorListMatcher[T](m: Matcher[Either[List[T], _]]): ErrorListMatcher[T] =
new ErrorListMatcher[T](m)
class ErrorListMatcher[T](m: Matcher[Either[List[T], _]]) {
// beLike checks one element
// beLike.atLeastOnce transforms that matcher on a
// matcher on a sequence of elements
def like[S](f: PartialFunction[T, MatchResult[S]]) = {
m and beLike(f).atLeastOnce ^^ ((e: Either[List[T], _]) => e.left.toOption.get)
}
With Scala's pattern matching I would like to confirm not only that two Strings are equal but for example, whether a String starts with, ends, or is contained in another etc.
I experimented with case classes and extractor objects, neither giving me a concise solution. So the solution I came up with looks like the following:
class StrMatches(private val str: Option[String]) {
def ^(prefix: String) = str.exists(_.startsWith(prefix))
def §(suffix: String) = str.exists(_.endsWith(suffix))
def %(infix: String) = str.exists(_.contains(infix))
def ~(approx: String) = str.exists(_.equalsIgnoreCase(approx))
def /(regex: scala.util.matching.Regex) = str.collect({ case regex() => true }).isDefined
def °(len: Int) = str.exists(_.length == len)
def °°(len: (Int, Int)) = str.exists(a => a.length >= len._1 && a.length <= len._2)
def `\\s*` = str.exists(_.trim.isEmpty)
override def toString = str.mkString
}
object StrMatches {
implicit def apply(x: Str) = new StrMatches(x)
def unapply(x: StrMatches) = x.str
implicit def unwrap(x: StrMatches) = x.toString
}
A client using the StrMatches class could look like the following:
object TestApp extends App {
val str = "foobar"
val strMatches = StrMatches(str)
if (strMatches ^ "foo") {
println(strMatches)
}
if (strMatches § "bar") {
println(strMatches)
}
if (strMatches % "ob") {
println(strMatches)
}
}
As opposed to writing:
object TestApp extends App {
val str: String = null // Just as an illustration for Scala interfacing Java.
if (str != null) {
if (str.startsWith("foo")) {
println(str)
}
if (strMatches.endsWith("bar")) {
println(str)
}
if (strMatches.contains("ob")) {
println(strMatches)
}
}
}
With what kind of solutions would you come up with?
You could use regular expressions. Then you could use pattern matching (which I think was the original intent of your question):
object TestApp extends App {
val str = "foobar"
val StartsWithFooRE = """^foo.*""".r
val EndsWithBarRE = """.*bar$""".r
val ContainsBoRE = """.*bo.*""".r
str match {
case StartsWithFooRE() => println(str)
case EndsWithBarRE() => println(str)
case ContainsBoRE() => println(str)
case _ =>
}
}
To make this more convenient, you could define an object with factory methods to construct the regular expressions. However, due to how pattern matching works, you'll still have to define the expressions outside of the match:
import scala.util.matching.Regex
object RegexFactory {
def startsWith(str: String) = new Regex("^%s.*" format str)
def endsWith(str: String) = new Regex(".*%s$" format str)
def contains(str: String) = new Regex(".*%s.*" format str)
}
object TestApp extends App {
val str = "foobar"
import RegexFactory._
val StartsWithFooRE = startsWith("foo")
val EndsWithBarRE = endsWith("bar")
val ContainsBoRE = contains("bo")
str match {
case StartsWithFooRE() => println(str)
case EndsWithBarRE() => println(str)
case ContainsBoRE() => println(str)
case _ =>
}
}