Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

scala: mixins depending on type of arguments

I have a set of classes of models, and a set of algorithms that can be run on the models. Not all classes of models can perform all algorithms. I want model classes to be able to declare what algorithms they can perform. The algorithms a model can perform may depend on its arguments.

Example: Say I have two algorithms, MCMC, and Importance, represented as traits:

trait MCMC extends Model {
  def propose...
}

trait Importance extends Model {
  def forward...
}

I have a model class Normal, which takes a mean argument, which is itself a Model. Now, if mean implements MCMC, I want Normal to implement MCMC, and if mean implements Importance, I want Normal to implement Importance.

I can write: class Normal(mean: Model) extends Model { // some common stuff goes here }

class NormalMCMC(mean: MCMC) extends Normal(mean) with MCMC {
  def propose...implementation goes here
}

class NormalImportance(mean: Importance) extends Normal(mean) with Importance {
  def forward...implementation goes here
}

I can create factory methods that make sure the right kind of Normal gets created with a given mean. But the obvious question is, what if mean implements both MCMC and Importance? Then I want Normal to implement both of them too. But I don't want to create a new class that reimplements propose and forward. If NormalMCMC and NormalImportance didn't take arguments, I could make them traits and mix them in. But here I want the mixing in to depend on the type of the argument. Is there a good solution?

like image 414
Avi Pfeffer Avatar asked Jan 10 '10 16:01

Avi Pfeffer


3 Answers

Using self types allows you to separate the Model-Algorithm implementations from the instantiations and mix them in:

trait Model
trait Result
trait MCMC extends Model {
  def propose: Result
}
trait Importance extends Model {
  def forward: Result
}

class Normal(val model: Model) extends Model

trait NormalMCMCImpl extends MCMC {
  self: Normal =>
  def propose: Result = { //... impl
    val x = self.model // lookie here... I can use vals from Normal
  }
}
trait NormalImportanceImpl extends Importance {
  self: Normal =>
  def forward: Result = { // ... impl
      ...
  }
}

class NormalMCMC(mean: Model) extends Normal(mean)
                              with NormalMCMCImpl

class NormalImportance(mean: Model) extends Normal(mean)
                                    with NormalImportanceImpl

class NormalImportanceMCMC(mean: Model) extends Normal(mean)
                                        with NormalMCMCImpl
                                        with NormalImportanceImpl
like image 120
Mitch Blevins Avatar answered Sep 19 '22 05:09

Mitch Blevins


Thanks to Kevin, Mitch, and Naftoli Gugenheim and Daniel Sobral on the scale-users mailing list, I have a good answer. The two previous answers work, but lead to an exponential blowup in the number of traits, classes and constructors. However, using implicits and view bounds avoids this problem. The steps of the solution are:

1) Give Normal a type parameter representing the type of its argument. 2) Define implicits that take a Normal with the right type of argument to one that implements the appropriate algorithm. For example, makeImportance takes a Normal[Importance] and produces a NormalImportance. 3) The implicits need to be given a type bound. The reason is that without the type bound, if you try to pass a Normal[T] to makeImportance where T is a subtype of Importance, it will not work because Normal[T] is not a subtype of Normal[Importance] because Normal is not covariant. 4) These type bounds need to be view bounds to allow the implicits to chain.

Here's the full solution:

class Model

trait Importance extends Model {
  def forward: Int
}

trait MCMC extends Model {
  def propose: String
}

class Normal[T <% Model](val arg: T) extends Model

class NormalImportance(arg: Importance) extends Normal(arg) with Importance {
  def forward = arg.forward + 1
}

class NormalMCMC(arg: MCMC) extends Normal(arg) with MCMC {
  def propose = arg.propose + "N"
}

object Normal {
  def apply[T <% Model](a: T) = new Normal[T](a)
}

object Importance {
  implicit def makeImportance[T <% Importance](n: Normal[T]): Importance = 
    new NormalImportance(n.arg)
}

object MCMC {
  implicit def makeMCMC[T <% MCMC](n: Normal[T]): MCMC = new NormalMCMC(n.arg)
}

object Uniform extends Model with Importance with MCMC {
  def forward = 4
  def propose = "Uniform"
}

def main(args: Array[String]) {
  val n = Normal(Normal(Uniform))
  println(n.forward) 
  println(n.propose)
}
like image 21
Avi Pfeffer Avatar answered Sep 18 '22 05:09

Avi Pfeffer


Much of your problem seems to be that NormalMCMC and NormalImportance take arguments but, as you correctly imply, traits can't have constructors.

Instead, you can take the parameters that you'd want to supply via a trait constructor (if such a thing existed) and make them abstract members of the trait.

The members then get realised when the trait is constructed.

Given:

trait Foo {
  val x : String //abstract
}

you can use it as either of the following:

new Bar with Foo { val x = "Hello World" }

new Bar { val x = "Hello World" } with Foo

Which gives you back the equivalent functionality of using Trait constructors.

Note that if the type Bar already has a non-abstract val x : String then you can simply use

new Bar with Foo

In some scenarios it can also help to make x lazy, which can gives you more flexibility if initialization order should become an issue.

like image 34
Kevin Wright Avatar answered Sep 20 '22 05:09

Kevin Wright