Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Field in a trait is not initialized in time

Tags:

scala

I can't figure out why the field encryptKey in the following code is not initialised to 3 by the time class constructor is called:

trait Logger{
  println("Construction of Logger")
  def log(msg: String) { println(msg) }
}

trait EncryptingLogger extends Logger {
  println("Construction of EncryptingLogger")
  val encryptKey = 3

  override def log(msg: String){
    super.log(msg.map(encrypt(_, encryptKey)))
  }

  def encrypt(c: Char, key: Int) =
    if (c isLower) (((c - 'a') + key) % 26 + 'a').toChar
    else if (c isUpper) (((c.toInt - 'A') + key) % 26 + 'A').toChar
    else c
}

class SecretAgent (val id: String, val name: String) extends Logger {
  println("Construction of SecretAgent")
  log("Agent " + name + " with id " + id + " was created.")
}

val bond = new SecretAgent("007", "James Bond") with EncryptingLogger

In my understanding, linearization of the created object would be:

SecretAgent -> EncryptingLogger -> Logger -> ScalaObject

and construction order goes from right to left, meaning that the variable should be already initialized before constructor of SecretAgent starts. But the prinln's tell me different:

scala> val bond = new SecretAgent("007", "James Bond") with EncryptingLogger
Construction of Logger
Construction of SecretAgent
Agent James Bond with id 007 was created.
Construction of EncryptingLogger
bond: SecretAgent with EncryptingLogger = $anon$1@49df83b5

I tried to mixin the same trait differently:

class SecretAgent (val id: String, val name: String) extends Logger with EncryptingLogger

and the variable is initialized in time:

scala> val bond = new SecretAgent("007", "James Bond")
Construction of Logger
Construction of EncryptingLogger
Construction of SecretAgent
Djhqw Mdphv Erqg zlwk lg 007 zdv fuhdwhg.
bond: SecretAgent = SecretAgent@1aa484ca

So what is the difference between mixing in the class definition and mixing in the object, could someone explain?

like image 348
Konstantin Milyutin Avatar asked Oct 29 '25 12:10

Konstantin Milyutin


1 Answers

The problem here is that you call a method in the constructor of your class, that accesses a field of the superclass/trait, before the super constructor is called. The easiest way to workaround that, is to make the field lazy, because it will then be evaluated, when it is first accessed:

trait Logger{
  def log(msg: String) { println(msg) }
}

trait EncryptingLogger extends Logger {
  lazy val encryptKey = 3

  override def log(msg: String){
    super.log(msg.map(encrypt(_, encryptKey)))
  }

  def encrypt(c: Char, key: Int) =
    if (c isLower) (((c - 'a') + key) % 26 + 'a').toChar
    else if (c isUpper) (((c.toInt - 'A') + key) % 26 + 'A').toChar
    else c
}

class SecretAgent (val id: String, val name: String) extends Logger {
  log("Agent " + name + " with id " + id + " was created.")
}


scala> val bond = new SecretAgent("007", "James Bond") with EncryptingLogger
Djhqw Mdphv Erqg zlwk lg 007 zdv fuhdwhg.
bond: SecretAgent with EncryptingLogger = $anon$1@4f4ffd2f

edit:

It becomes clear what happens here, when we look at the decompiled java code for

class Foo extends SecretAgent("007", "James Bond") with EncryptingLogger

where the constructor looks like this

public Foo() { 
  super("007", "James Bond");
  EncryptingLogger.class.$init$(this);
}

As you can see, it first calls the super constructor (in this case SecretAgent) which calls the log method and after that it calls the init method of the EncryptionLogger. Therefore the encryptKey still has its default value, which is 0 for integers.

If you now make the encryptKey field lazy, the getter will look like this:

public int encryptKey() {
  return this.bitmap$0 ? this.encryptKey : encryptKey$lzycompute();
}

and at first access it calls the following method to set the field to its value:

private int encryptKey$lzycompute() {
  synchronized (this) {
    if (!this.bitmap$0) {
      this.encryptKey = EncryptingLogger.class.encryptKey(this);
      this.bitmap$0 = true;
    }
    return this.encryptKey;
  }
}

edit2:

To answer your question about the order of constructors, it actually calls the constructors in the correct order. When you create an anonymous instance, it actually is like

class Anonymous extends SecretAgent with EncryptingLogger

in this case, the constructor of SecretAgent will be called first. If you extend the class SecretAgent with the trait, it will call the super constructors first. It behaves absolutely as expected. So if you want to use traits as mixins for anonymous instances, you have to be careful about the initialization order and here it helps to make fields, that are likely to be accessed in constructors lazy.

like image 97
drexin Avatar answered Nov 01 '25 01:11

drexin



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!