19

Let's say a I have a class:

case class Foo(id: Int, name: String, note: Option[String] = None)

Both the constructor and the apply method in the automatically generated companion object take three parameters. When viewed via reflection, the third parameter (note) is flagged:

p.isParamWithDefault = true

Also, by inspection I can find the method that produces the value in the companion object:

method <init>$default$3

and

method apply$default$3

Which both also have:

m.isParamWithDefault = true

However, I can neither find anything on the TermSymbol for the notes parameter that actually points me at the right methods to obtain the default value nor anything on the above MethodSymbols that point back to the TermSymbol for the parameter.

Is there a straight forward way to link TermSymbol for the parameter with the method that generates its default value? Or do I need to do something kludgey like inspect the names of the methods on the companion object?

I'm interested in this both for the case class constructor example I have here and for regular methods.

Erik Engbrecht
  • 3,174
  • 17
  • 23
  • There are degrees of kludge. Sample code at [this answer.][1] [1]: http://stackoverflow.com/a/13813000/1296806 – som-snytt Dec 25 '12 at 23:45
  • Yeah, I've coded something similar to that. But it relies on Scala's name mangling scheme which *should* be treated as an implementation detail. In fact there a thread on it going on right now: https://groups.google.com/d/topic/scala-internals/aE81MVdIhCk/discussion – Erik Engbrecht Dec 25 '12 at 23:51
  • Yes, also following the thread; my interest stemmed from a PR that needed to go from default args to the method, etc, it's messy interally, let alone externally. But as noted, the form of the mangle is spec'd. – som-snytt Dec 25 '12 at 23:58
  • If I understand correctly name mangling for default argument methods is specified in the SID for named and default arguments but it is not in the SLS. I'm not sure, but I don't believe SIDs are treated as hard specifications to which the implementation must conform. They're more general design documents. – Erik Engbrecht Dec 26 '12 at 00:35
  • My answer edit includes both SLS locs. You'd think it would be impl detail. My use case is, I make a dumb exception class with an optional message, I'd hate to generate a companion just for the default arg. Alternative would be to overload the constructor, as you'd normally do by hand. – som-snytt Dec 26 '12 at 01:43

2 Answers2

9

There are degrees of kludge.

Sample code at this answer, pasted below.

So as I was saying, the form of the name is in the spec at 4.6, 6.6.1. That is not ad-hoc. For every parameter pi , j with a default argument a method named f $default$n is generated which computes the default argument expression.

The lack of structured ability to access and reconstitute these generated names is a known issue (with a current thread on the ML).

import reflect._
import scala.reflect.runtime.{ currentMirror => cm }
import scala.reflect.runtime.universe._

// case class instance with default args

// Persons entering this site must be 18 or older, so assume that
case class Person(name: String, age: Int = 18) {
  require(age >= 18)
}

object Test extends App {

  // Person may have some default args, or not.
  // normally, must Person(name = "Guy")
  // we will Person(null, 18)
  def newCase[A]()(implicit t: ClassTag[A]): A = {
    val claas = cm classSymbol t.runtimeClass
    val modul = claas.companionSymbol.asModule
    val im = cm reflect (cm reflectModule modul).instance
    defaut[A](im, "apply")
  }

  def defaut[A](im: InstanceMirror, name: String): A = {
    val at = newTermName(name)
    val ts = im.symbol.typeSignature
    val method = (ts member at).asMethod

    // either defarg or default val for type of p
    def valueFor(p: Symbol, i: Int): Any = {
      val defarg = ts member newTermName(s"$name$$default$$${i+1}")
      if (defarg != NoSymbol) {
        println(s"default $defarg")
        (im reflectMethod defarg.asMethod)()
      } else {
        println(s"def val for $p")
        p.typeSignature match {
          case t if t =:= typeOf[String] => null
          case t if t =:= typeOf[Int]    => 0
          case x                         => throw new IllegalArgumentException(x.toString)
        }
      }
    }
    val args = (for (ps <- method.paramss; p <- ps) yield p).zipWithIndex map (p => valueFor(p._1,p._2))
    (im reflectMethod method)(args: _*).asInstanceOf[A]
  }

  assert(Person(name = null) == newCase[Person]())
}
Community
  • 1
  • 1
som-snytt
  • 39,429
  • 2
  • 47
  • 129
  • Thanks for adding the references to the SLS. When I had checked it I had missed it. I was hoping for something cleaner, but at least the appears to rely on public interfaces and specified conventions. – Erik Engbrecht Dec 26 '12 at 02:03
  • You should compare `TypeTag`s with the `=:=` operator instead of just `==`, when you match on `p.typeSignature` (see http://stackoverflow.com/a/12232195/3714539, around `res30`/`res31`) – Alex Archambault Jul 24 '14 at 22:40
  • Is it possible to get the default value using compile-time reflection (i.e. from within a macro)? – josephpconley Feb 09 '15 at 22:01
  • @josephpconley The methods are added early, in namer phase, so, yes. – som-snytt Feb 10 '15 at 05:03
  • The `MethodMirror` you get from `im reflectMethod` is a runtime mirror, is there an alternative way to invoke a `MethodSymbol` at compile-time? – josephpconley Feb 10 '15 at 11:59
  • @josephpconley You want to get the tree for the method body and invoke `context.eval(tree)`, if by "get the value" you mean exactly that. Suggest you open a question about it. – som-snytt Feb 10 '15 at 17:56
4

You can do this without making assumptions about the generated names—by casting to the internal API:

scala> :power
** Power User mode enabled - BEEP WHIR GYVE **
** :phase has been set to 'typer'.          **
** scala.tools.nsc._ has been imported      **
** global._, definitions._ also imported    **
** Try  :help, :vals, power.<tab>           **

scala> case class Foo(id: Int, name: String, note: Option[String] = None)
defined class Foo

scala> val t = typeOf[Foo.type]
t: $r.intp.global.Type = Foo.type

scala> t.declaration(nme.defaultGetterName(nme.CONSTRUCTOR, 3))
res0: $r.intp.global.Symbol = method <init>$default$3

scala> t.declaration(nme.defaultGetterName(newTermName("apply"), 3))
res1: $r.intp.global.Symbol = method apply$default$3

Of course in a way this isn't any nicer, since the name mangling is specified and the internal API isn't, but it may be more convenient.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • Fixing the encoding for constructor was my doing. Note the anomaly: `scala> nme.defaultGetterName(nme.MIXIN_CONSTRUCTOR, 3)` is `res0: $r.intp.global.TermName = $lessinit$greater$default$3`. By anomaly, I mean bug if it mattered. So arguably, it's better to use the implementation-dependent, anomaly-preserving API. – som-snytt Dec 26 '12 at 00:34