Macro puzzler

200 views
Skip to first unread message

etorreborre

unread,
Oct 10, 2014, 1:23:07 AM10/10/14
to scala...@googlegroups.com
I have written a small macro to override the toString method of a case class and display the member names (link).

This works fine as long as I call the method from a single instance (shown in this spec).

However, as soon as I call the toString method through a List or an Option toString method:

case class Point(x: Int, y: Int) {
override def toString: String =
macro ToString.toStringWithNames
}

println(Option(Point(1, 2)))

Then I get:

Some(Point@5428bd62)

as if the overridden method had switched to the parent method.

This probably stems from a misunderstanding from my side on how macros work but this is a very surprising behaviour (tested with Scala 2.11.2).

Does anyone have an idea of what is wrong?

Thanks,

Eric.

Jason Zaugg

unread,
Oct 10, 2014, 1:53:42 AM10/10/14
to etorreborre, scala-user
You should instead use:
  override def toString = ToString.toStringWithNames[Point].

Not only will this work as expected when you invoke through the superclass method, it will also only need to expand the macro once, rather than at each call site of toString.

To avoid explicitly passing in the type argument, you could inspect the enclosing owner (via a new, power user API in 2.11)
scala> def whereAmIImpl(c: Context): c.Tree= { import c.universe._; q"${c.internal.enclosingOwner.owner.toString}"}
whereAmIImpl: (c: scala.reflect.macros.blackbox.Context)c.Tree

scala> def whereAmI: String = macro whereAmIImpl
defined term macro whereAmI: String

scala> class C { def foo = whereAmI }
defined class C

scala> new C().foo
res3: String = class C
-jason

--
You received this message because you are subscribed to the Google Groups "scala-user" group.
To unsubscribe from this group and stop receiving emails from it, send an email to scala-user+...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

etorreborre

unread,
Oct 10, 2014, 2:04:24 AM10/10/14
to scala...@googlegroups.com, etorr...@gmail.com
> ​To avoid explicitly passing in the type argument, you could inspect the enclosing owner (via a new, power user API in 2.11)
I tried to do that at first but that returns the enclosing owner at the call site (a Specification for example) and not the owner of the method (a Point).
But there should be a way to access the type at the definition site, right?

Jason Zaugg

unread,
Oct 10, 2014, 2:09:44 AM10/10/14
to etorreborre, scala-user
On Fri, Oct 10, 2014 at 4:04 PM, etorreborre <etorr...@gmail.com> wrote:
> ​To avoid explicitly passing in the type argument, you could inspect the enclosing owner (via a new, power user API in 2.11)
I tried to do that at first but that returns the enclosing owner at the call site (a Specification for example) and not the owner of the method (a Point).
But there should be a way to access the type at the definition site, right?

IMO the only time you should call this macro to generate a to-stringer for Point is in the implementation of Point#toString. Call sites of then just call a regular toString method.

We should probably warn if you override a method with a macro def. Maybe there are some valid use cases for that, but it is mostly done in error.

-jason


etorreborre

unread,
Oct 10, 2014, 2:51:36 AM10/10/14
to scala...@googlegroups.com, etorr...@gmail.com
So you recommend to either:

 - use a type tag and get the expected type 

    -> that doesn't seem to work when the Point instance is inside an Option
 
 - defer to another method like this:

case class Point(x: Int, y: Int) {
  override def toString: String = string
def string = macro ToString.toStringWithNames
} 

This works fine. That would be nice if there was a way to reduce a bit more the boilerplate though.

Jason Zaugg

unread,
Oct 10, 2014, 3:00:46 AM10/10/14
to etorreborre, scala-user
On Fri, Oct 10, 2014 at 4:51 PM, etorreborre <etorr...@gmail.com> wrote:
So you recommend to either:

 - use a type tag and get the expected type 

    -> that doesn't seem to work when the Point instance is inside an Option
 
 - defer to another method like this:

case class Point(x: Int, y: Int) {
override def toString: String = string
def string = macro ToString.toStringWithNames
} 


I'm pretty certain you can rework your macro along the lines of ScalaEquals (or just use that macro!) .

-jason

Johannes Rudolph

unread,
Oct 10, 2014, 3:51:43 AM10/10/14
to etorreborre, scala...@googlegroups.com

On Friday, October 10, 2014, etorreborre <etorr...@gmail.com> wrote:
case class Point(x: Int, y: Int) {
override def toString: String =
macro ToString.toStringWithNames
}

This should be a compile-time error IMO. At least, because this macro method actually doesn't override anything.


--
Johannes

-----------------------------------------------
Johannes Rudolph
http://virtual-void.net

etorreborre

unread,
Oct 10, 2014, 7:50:25 AM10/10/14
to scala...@googlegroups.com, etorr...@gmail.com
> I'm pretty certain you can rework your macro along the lines of ScalaEquals (or just use that macro!) .

I tried ScalaEquals and got the same result. This is not surprising to me because I'm doing something completely similar.

When I googled about changing the toString method I realised that there was an old issue on the subject. You suggest there to possibly use a macro annotation for it. 
It was 2 years ago, do you think it would work now?

Thanks for your help,

Eric.

Jason Zaugg

unread,
Oct 10, 2014, 8:02:54 AM10/10/14
to etorreborre, scala-user
On Fri, Oct 10, 2014 at 9:50 PM, etorreborre <etorr...@gmail.com> wrote:
> I'm pretty certain you can rework your macro along the lines of ScalaEquals (or just use that macro!) .

I tried ScalaEquals and got the same result. This is not surprising to me because I'm doing something completely similar.

I'm not quite sure what the issue is. What did you mean by " that doesn't seem to work when the Point instance is inside an Option"

Can you distill the essence of it down to something we can discuss here?

-jason

etorreborre

unread,
Oct 10, 2014, 8:16:09 AM10/10/14
to scala...@googlegroups.com, etorr...@gmail.com
Sure.

Let's say I define this macro:

object ToString {

  def toStringWithNames(c: Context): c.Expr[String] = {
    import c.universe.{getClass =>_,_}

    // class for which we want to display toString
    val klass = c.macroApplication.symbol.owner
    val className = simpleName(klass.name.decodedName.toString)

    // we keep the getter fields created by the user
    val fields: Iterable[c.Symbol] = klass.asClass.toType.members
      .filter(_.isPublic) // we should do more filtering here

    /** some useful literals */
    val equal = q"""" = """"
    val emptyString: String = ""
    val empty = q"""$emptyString"""
    val tab = q""" "  " """
    val newline = q""" "\n" """

    // print one field as <name of the field>+"="+fieldName
    def printField(field: Symbol) = {
      val fieldName = field.name.decodedName.toString
      q"""$fieldName+$equal+${Select(c.prefix.tree, createTermName(c)(fieldName))} """
    }

    // fold over the fields to create an expression like
    // "" +
    // <name of the field1>+"="+fieldName1 +
    // <name of the field1>+"="+fieldName2 + ...
    val parameters = fields.foldLeft(q"$empty") { (res, field) => q"$tab + ${printField(field)} + $newline + $res" }

    // print the class and all the parameters with their values
    c.Expr(q"""$className + "(\n" + $parameters + ")" """)
  }

  def simpleName(name: String): String =
    name.split("\\.").lastOption.getOrElse("<none>").split("\\$").headOption.getOrElse("<none>")
}


Then if I defined the following case class:

case class Point(x: Int, y: Int) {
  override def toString: String = show
  def show: String = macro ToString.toStringWithNames
}

And print out Option(Point(1, 2)).toString, I see something like:

 Some(Point(
   x = 1
   y = 2
   ...))

Now If I define the case class Point as:

case class Point(x: Int, y: Int) {
  override def toString: String = 
    macro ToString.toStringWithNames
}

Then the output of Option(Point(1, 2)).toString is something like Some(Point@5428bd62) (but Point(1, 2).toString is "Point(x = 1, y = 2, ...)).

My aim is to implement the most concise way to create a toString method displaying the field names for a case class so that toString can be called in any context:

 point.toString, Option(point).toString, List(point1, point2).mkString(",")

etorreborre

unread,
Oct 10, 2014, 8:43:33 AM10/10/14
to scala...@googlegroups.com, etorr...@gmail.com
PS: at least I now understand why my current version behaves as it does. It's not a puzzler anymore :-)

Jason Zaugg

unread,
Oct 10, 2014, 9:16:12 AM10/10/14
to etorreborre, scala-user

On Fri, Oct 10, 2014 at 10:43 PM, etorreborre <etorr...@gmail.com> wrote:

PS: at least I now understand why my current version behaves as it does. It's not a puzzler anymore :-)

:)

Here’s what I would do:

==> sandbox/macro.scala <==
import scala.reflect.macros.blackbox._
import scala.language.experimental._

object ToString {
  def toStringWithNames: String = macro toStringWithNamesImpl
  def toStringWithNamesImpl(c: Context): c.Tree = {
    import c.universe._

    
// class for which we want to display toString

    val klass = c.internal.enclosingOwner.owner

    
// we keep the getter fields created by the user

    val fields: Iterable[c.Symbol] = klass.asClass.toType.decls
      .filter(sym => sym.isMethod && sym.asTerm.isParamAccessor) // we should do more filtering here


    // print one field as <name of the field>+"="+fieldName
    def printField(field: Symbol) = {
      val fieldName = field.name

      q"""${fieldName.decoded.toString}+${"="}+this.$field"""
    }
    val params = fields.foldLeft(q"${""}")((res, acc) => q"${printField(acc)} + $res")

    
// print the class and all the parameters with their values

    q"this.productPrefix + ${"("} + $params + ${")"}"
  }
}

==> sandbox/test.scala <==
case class Point(x: Int) {
  
override def toString = ToString.toStringWithNames
}

object Test extends App {
  println(Some(Point(1)).toString)
}
% scalac-hash v2.11.2 sandbox/macro.scala && scalac-hash v2.11.2  sandbox/test.scala && scala-hash v2.11.2 Test
warning: there was one deprecation warning; re-run with -deprecation for details
one warning found
Some(Point(x=1))

-jason

etorreborre

unread,
Oct 11, 2014, 12:05:55 AM10/11/14
to scala...@googlegroups.com, etorr...@gmail.com
That works great, thanks a lot!
Reply all
Reply to author
Forward
0 new messages