Majakovskij
Majakovskij

Reputation: 575

How to do early initializations of variables in Scala?

I have an architectural problem, more precisely, a suboptimal situation.

For an adaptable test environment, there is a context that is updated by a range of definition methods, which each define different entities, i.e. alter the context. For simplicity, the definitions here will just be integers, and the context a growing Seq[Int].

trait Abstract_Test_Environment {
    def definition(d: Int): Unit
    /* Make definitions: */
    definition(1)
    definition(2)
    definition(3)
}

This idea is now implemented by a consecutively altered “var” holding the current context:

trait Less_Abstract_Test_Environment extends Abstract_Test_Environment {
    /* Implement the definition framework: */
    var context: Context = initial_context
    val initial_context: Context
    override def definition(d: Int) = context = context :+ d
}

Since the context must be set before “definition” is applied, it cannot be set by variable assignment in the concluding class:

class Concrete_Test_Environment extends Less_Abstract_Test_Environment {
    context = Seq.empty
}

An intermediate “initial_context” is required but a plain overriding does not do the job either:

class Concrete_Test_Environment extends Less_Abstract_Test_Environment {
    override val initial_context = Seq.empty
}

The only viable solution seems to be an early initialization, which most likely is the purpose this feature has been created for:

class Concrete_Test_Environment extends {
    override val initial_context = Seq.empty
} with Less_Abstract_Test_Environment

HOWEVER, our setting still fails because when “definition” is applied in “Abstract_Test_Environment”, the VAR “context” in “Less_Abstract_Test_Environment” is still not bound, i.e. null. Whereas the def “definition” is “initialized on demand” in “Less_Abstract_Test_Environment” (from “Abstract_Test_Environment”), the var “context” is not.

The “solution” I came up with is merging “Abstract_Test_Environment” and “Less_Abstract_Test_Environment”. This is not what I wanted since it destroys the natural separation of interface/goal and implementation, which has been realized by the two traits.

Do you see any better solution? I am sure Scala can do better.

Upvotes: 0

Views: 291

Answers (3)

Richard Gomes
Richard Gomes

Reputation: 6094

You can employ the idea of a whiteboard, which contains only data, which is shared by a number of traits which contain only logic (not data!). See below some untested code off the cuff:

trait WhiteBoard {
  var counter: Int = 0
}

trait Display {
  var counter: Int
  def show: Unit = println(counter)
}

trait Increment {
  var counter: Int
  def inc: Unit = { counter = counter + 1 }
}

Then you write unit tests like this:

val o = new Object with Whiteboard with Display with Increment
o.show
o.inc
o.show

Doing this way, you separate definition of the data from places where the data is required, which basically means that you can potentially mix in traits in any order. The only requirement is that the whiteboard (which defines data) is the first trait mixed in.

Upvotes: 0

Dima
Dima

Reputation: 40510

One possibility is to make your intermediate trait a class:

abstract class Less_Abstract_Test_Environment(var context: Context = Seq.empty) extends Abstract_Test_Environment {
   override def definition(d: Int) = context = context :+ d
}

You can now subclass it, and pass different initial contexts in as parameters to constructor.

You can do this at the "concrete" level too, if you'd rather have the intermediate as a trait:

trait Less_Abstract_Test_Environment extends Abstract_Test_Environment {
   var context: Context     
   override def definition(d: Int) = context = context :+ d
}

class Concrete_Test_Environment(override var context: Context = Seq.empty) extends Less_Abstract_Test_Environment

What would be even better though is using functional approach: context should be a val, and definion should take the previous value, and return the new one:

    trait Abstract {
       type Context
       def initialContext: Context
       val context: Context = Range(1, 4)
         .foldLeft(initialContext) { case (c, n) => definition(c, n) }  
       def definition(context: Context, n: Int): Context

    }

    trait LessAbstract extends Abstract {
        override type Context = Seq[Int]
        override def definition(context: Context, n: Int) = context :+ n
    }

    class Concrete extends LessAbstract {
       override def initialContext = Seq(0)
    }

Upvotes: 1

kiritsuku
kiritsuku

Reputation: 53358

Simple solution: Do not initialize your object during its creation, except you are in the bottom level class. Instead, add an init method, which contains all of the initialization code and then call it either in the most bottom level class (which is safe, since all parent classes have already been created) or wherever the object is created.

Side effect of the whole thing is that you can even override the initialization code in a subclass.

Upvotes: 1

Related Questions