0

I am creating an object of a class in Scala and I would like to clone the object but change one member's value. For that I'm trying to do something like this:

abstract class A {
  var dbName: String

  def withConfig(db: String): A = {
    var a = this
    a.dbName = db
    a
  }
}

class A1(db: String) extends A {
  override var dbName: String = db
}

class A2(db: String) extends A {
  override var dbName: String = db
}

object Test {
  def main(args: Array[String]): Unit = {
    var obj = new A1("TEST")
    println(obj.dbName)
    var newObj = obj.withConfig("TEST2")
    println(newObj.dbName)
    println(obj.dbName)
  }
}

When I run this program, I get the below output:

TEST
TEST2
TEST2

While I was able to create a new object from the original in this, unfortunately it ended up modifying the value of the member of the original object as well. I believe this is because I'm using the same instance this and then changing the member.

Thus I thought I can clone the object instead, for which I made a change to the withConfig method:

def withConfig(db: String): A = {
    var a = this.clone().asInstanceOf[A]
    a.dbName = db
    a
}

Unfortunately, it is throwing an error saying Clone Not supported:

TEST
Exception in thread "main" java.lang.CloneNotSupportedException: c.i.d.c.A1
    at java.lang.Object.clone(Native Method)
    at c.i.d.c.A.withConfig(Test.scala:7)
    at c.i.d.c.Test$.main(Test.scala:21)
    at c.i.d.c.Test.main(Test.scala)

Is there a way I could achieve the functionality similar to clone(), but without making significant changes to the abstract class?

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Sparker0i
  • 1,787
  • 4
  • 35
  • 60

1 Answers1

1

You should override clone in classes

abstract class A extends Cloneable {
  var dbName: String

  def withConfig(db: String): A = {
    var a = this.clone().asInstanceOf[A]
    a.dbName = db
    a
  }
}

class A1(db: String) extends A {
  override var dbName: String = db
  override def clone(): AnyRef = new A1(db)
}

class A2(db: String) extends A {
  override var dbName: String = db
  override def clone(): AnyRef = new A2(db)
}

By the way, using vars is not idiomatic.


With vals instead of vars (and not using Java Cloneable at all)

abstract class A {
  def db: String
  def withConfig(db: String): A
}

class A1(val db: String) extends A {
  override def withConfig(db: String): A = new A1(db)
}

class A2(val db: String) extends A {
  override def withConfig(db: String): A = new A2(db)
}

val obj = new A1("TEST")
println(obj.db) // TEST
val newObj = obj.withConfig("TEST2")
println(newObj.db) // TEST2
println(obj.db) // TEST

or (with withConfig returning more precise type than A)

abstract class A {
  def db: String
  type This <: A
  def withConfig(db: String): This
}

class A1(val db: String) extends A {
  override type This = A1
  override def withConfig(db: String): This = new A1(db)
}

class A2(val db: String) extends A {
  override type This = A2
  override def withConfig(db: String): This = new A2(db)
}

I would even implement This and withConfig with a macro annotation

import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

@compileTimeOnly("enable macro annotations")
class implement extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro ImplementMacro.impl
}

object ImplementMacro {
  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"$mods type $tpname[..$tparams] = $tpt" => tq"$tpname"
        }
        q"""
          $mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self =>
            ..$stats
            override type This = $tpname[..$tparams1]
            override def withConfig(db: String): This = new $tpname(db)
          }

          ..$tail
        """
    }
  }
}
abstract class A {
  def db: String
  type This <: A
  def withConfig(db: String): This
}

@implement
class A1(val db: String) extends A

@implement
class A2(val db: String) extends A

//scalac: {
//  class A1 extends A {
//    <paramaccessor> val db: String = _;
//    def <init>(db: String) = {
//      super.<init>();
//      ()
//    };
//    override type This = A1;
//    override def withConfig(db: String): This = new A1(db)
//  };
//  ()
//}
//scalac: {
//  class A2 extends A {
//    <paramaccessor> val db: String = _;
//    def <init>(db: String) = {
//      super.<init>();
//      ()
//    };
//    override type This = A2;
//    override def withConfig(db: String): This = new A2(db)
//  };
//  ()
//}

Settings: Auto-Generate Companion Object for Case Class in Scala

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Thanks for this, it helped. Unfortunately, I've inherited a code base where the code is written in Scala language, but all the coding patterns are very similar to Java EE projects which do not take advantage of Scala's FP features. That's why I did not want to make too many changes to the `var`s. – Sparker0i Dec 04 '22 at 17:35
  • That being said, may I ask how would you would change the `var`s, but also be able to accomplish what I had asked in the question? – Sparker0i Dec 04 '22 at 17:35
  • Ah nice. Retains immutability of the member and also creates a new object without modifying the existing one. Thanks for this :) – Sparker0i Dec 04 '22 at 18:53
  • Yeah did see that. Macros would be way too difficult for others to digest (myself have to learn more about it as well), but the other suggestion should be doable.. – Sparker0i Dec 04 '22 at 18:55