RAQ
RAQ

Reputation: 129

Call class contructor before trait's constructor in scala

I have this scenario

trait D {
    def someMethod(): Unit = {}
  }

  trait C {
    val locations: Seq[Int]

    someSomethingWithLocations() // calling this in constructor

    def someSomethingWithLocations(): Unit = {
      print(locations.length)
    }
  }

  class B extends D with C {
    override val locations: Seq[Int] = 1 :: 2 :: 3 :: Nil
    someMethod()
  }

  def main(args: Array[String]): Unit = {
    val b = new B
  }

When I run this code someSomethingWithLocations throws null pointer exception because constructor of class B is not called yet, hence locations is not initialized. And If I change declaration of class B to

      class B extends{
        override val locations: Seq[Int] = 1 :: 2 :: 3 :: Nil
        someMethod()
      } with D with C 

compiler complains that someMethod() is not found. How can i resolve this issue?

For now I have moved declaration of locations to a different trait and my program works as expected but I would like to avoid unnecessary trait if possible.

Upvotes: 0

Views: 313

Answers (1)

ghik
ghik

Reputation: 10764

The solution you tried is so called early initializer. You can't call someMethod inside it, because:

  • early initializers can only contain val definitions
  • the trait D which contains someMethod is mixed in after the early initializer, so it can't be used yet

But anyway, early initializers should be considered a last resort when fixing initialization order. Before falling back to it, you should first try some less hacky solutions:

  1. Instead of defining or overriding a val from trait, try making it a constructor parameter. Constructor parameters which are vals are initialized before any constructor code is invoked. Here you can do it by introducing an intermediate abstract class:

    abstract class AbstractB(val locations: Seq[Int])
    class B extends AbstractB(1 :: 2 :: 3 :: Nil) with D with C {
      someMethod()
    }
    
  2. Make your val a lazy val:

    class B extends D with C {
      override lazy val locations: Seq[Int] = 1 :: 2 :: 3 :: Nil
      someMethod()
    }
    

    This way locations will be initialized not in the constructor of class B but simply on first access, which in your case will be the constructor of trait C (note that in this case the lazy keyword makes the field be initialized earlier than normally rather than later, which is a common intuition about lazy vals).

    lazy val seems like a simple and easy solution, but I would recommend first trying constructor parameters if possible. This is because the lazy val itself may access another val which may not yet be initialized at that point. This way the problem escalates to other vals and in the end you may find yourself having to declare all of them as lazy.

  3. If you still want to use an early initializer, you need to move the method call outside of it and put it into constructor:

    class B extends {
      override val locations: Seq[Int] = 1 :: 2 :: 3 :: Nil
    } with D with C {
      someMethod()
    }
    

Upvotes: 3

Related Questions