Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I add a no-arg constructor to a Scala case class with a macro annotation?

I'm trying to answer this question.

Instead of writing:

case class Person(name: String, age: Int) {
  def this() = this("",1)
}

I thought I'd use macro annotations to expand it from:

@Annotation
case class Person(name: String, age: Int)

So I tried adding the new constructor as a plain-old DefDef using quasiquotes in a macro annotation's impl, like:

val newCtor = q"""def this() = this("", 1)"""
val newBody = body :+ newCtor
q"$mods class $name[..$tparams](..$first)(...$rest) extends ..$parents { $self => ..$newBody }"

But that returns an error: called constructor's definition must precede calling constructor's definition

Is there a way to fix that? What did I miss?

Thanks for taking a look, -Julian

like image 433
Julian Peeters Avatar asked Mar 31 '14 08:03

Julian Peeters


1 Answers

It turns out that a very natural intent to generate a secondary constructor in a macro annotation has exposed two different issues.

1) The first issue (https://issues.scala-lang.org/browse/SI-8451) is about quasiquotes emitting wrong tree shapes for secondary constructors. That was fixed in 2.11.0-RC4 (yet to be released, currently available as 2.11.0-SNAPSHOT) and in paradise 2.0.0-M6 for 2.10.x (released yesterday).

2) The second issue is about unassigned positions wreaking havoc during typechecker. Curiously enough, when typechecking calls to constructors, typer uses positions in order to decide whether these calls are legal or not. That can't be patched that easily, and we have to work around:

         val newCtor = q"""def this() = this(List(Some("")))"""
-        val newBody = body :+ newCtor
+
+        // It looks like typer sometimes uses positions to decide whether stuff
+        // (secondary constructors in this case) typechecks or not (?!!):
+        // https://github.com/xeno-by/scala/blob/c74e1325ff1514b1042c959b0b268b3c6bf8d349/src/compiler/scala/tools/nsc/typechecker/Typers.scala#L2932
+        //
+        // In general, positions are important in getting error messages and debug
+        // information right, but maintaining positions is too hard, so macro writers typically don't care.
+        //
+        // This has never been a problem up until now, but here we're forced to work around
+        // by manually setting an artificial position for the secondary constructor to be greater
+        // than the position that the default constructor is going to get after macro expansion.
+        //
+        // We have a few ideas how to fix positions in a principled way in Palladium,
+        // but we'll have to see how it goes.
+        val defaultCtorPos = c.enclosingPosition
+        val newCtorPos = defaultCtorPos.withEnd(defaultCtorPos.endOrPoint + 1).withStart(defaultCtorPos.startOrPoint + 1).withPoint(defaultCtorPos.    point + 1)
+        val newBody = body :+ atPos(newCtorPos)(newCtor)
like image 179
Eugene Burmako Avatar answered Oct 19 '22 09:10

Eugene Burmako