0

Working in a codebase with scala that wants you, for certain classes, to define a sort of "make a new version" - so for instance if you have a class x(a :int, b:String, c:double)... it would have a function like this:

class x( a: Integer, b : String, c : Double) extends CanMakeNew
{
    def newValue() = x( a, b, c)
}

I have no control over that - but would prefer not to implement it every time. Or, well... ever. Is there a way in scala, with reflection - to iterate over the constructor parameter values? I can use reflection to look at the parameter types - but as parameternames has not been turned on for this module, and I can't turn it on - I can't correlate those with the stored values in the class. Fundamentally, I'm looking for anyway of implementing a trait like:

trait CanMakeNewDoneForMe extends CanMakeNew {
    def newValue()  {I need the code that goes here}

So does scala reflection have any way of either inspecting the constructor or inspecting the object and seeing "ahh, this was the third parameter in the constructor"?

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Darren Oakey
  • 2,894
  • 3
  • 29
  • 55

2 Answers2

5

If you make X a case class it will have apply, copy... generated by compiler automatically.

And basically it’s not my codebase, so I can’t really change any of the shape of things...

When you make a class a case class you don't actually "change shape of things", you just add autogenerated methods.

Anyway, you can create a macro annotation that generates method newValue.

  import scala.annotation.StaticAnnotation
  import scala.language.experimental.macros
  import scala.reflect.macros.blackbox

  class newValue extends StaticAnnotation {
    def macroTransform(annottees: Any*): Any = macro newValueMacro.impl
  }

  object newValueMacro {
    def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
      import c.universe._
      annottees match {
        case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" :: tail =>
          val tparams1 = tparams.map {
            case q"$_ type $name[..$_] >: $_ <: $_" => tq"$name"
          }
          val paramss1 = paramss.map(_.map {
            case q"$_ val $pat: $_ = $_" => pat
          })
          q"""
              $mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self =>
                def newValue() = new $tpname[..$tparams1](...$paramss1)
                ..$stats
              }

              ..$tail
            """

        case _ => c.abort(c.enclosingPosition, "not a class")
      }

    }
  }

  @newValue
  /*case*/ class X(a: Int, b : String, c : Double) {
    override def toString: String = s"X($a, $b, $c)"
  }

  val x = new X(1, "a", 2.0) //X(1, a, 2.0)
  //  val x1 = x.copy()
  val x1 = x.newValue() //X(1, a, 2.0)
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
-2

I might be wrong here, but usually this is achieved using pattern matching and the apply() and unapply() methods defined in your companion object.

I've done a small test of your above code in a REPL session. I didn't understand the purpose of the newvalue() function so I skipped it.

class x(val a: Integer, val b : String, val c : Double)
{
  //def newValue() = x( a, b, c)
}

object x {
  def apply(a: Integer, b: String, c: Double): x = new x(a,b,c)
  def unapply(m: x): Option[(Integer, String, Double)] = Some((m.a, m.b, m.c))
}

x(1, "hello", 99.0d) match {
  case l: x => println(s"this the the 3rd arg in the constructor: ${l.c}")
}

The unapply() function def above allows the pattern match deconstruct on the object. The alternative is to use a case class to define class x (this will define the apply() and unapply() functions for you).

Kevin Lawrence
  • 62
  • 1
  • 10
  • Sorry I wasn’t clear. The question is - how do I implement the newvalue function in a generic way - that’s the only thing that matters. – Darren Oakey Jul 15 '19 at 08:27
  • @DarrenOakey Why can't you make `X` a **case** class? It has `apply`, `copy`... generated by compiler automatically. – Dmytro Mitin Jul 15 '19 at 08:29
  • Because I haven’t shown the whole picture - these are all service classes that get initialised into an environment, and the function exists so they can get initialised into another environment. And basically it’s not my codebase, so I can’t really change any of the shape of things - I just want to find a clever way of not having to code that function by hand every time - and i can’t imagine a solution exists that doesn’t involve reflection :( – Darren Oakey Jul 15 '19 at 08:47