Question

I have a simple class Feature, currently implemented as a case class

case class Feature(value :String)

There are multiple operations decorating a feature with different properties, for example there is a function that might count the number of appearances of the feature so then I might need a CountedFeature. Besides counting I might need also a WeightedFeature, an IndexedFeature and so on.

My intuition says it's suitable for traits so I defined the following traits

trait Counted {def count :Long}
trait Weighted {def weight :Double}
trait Indexed {def index :Int}

Two issues pop up with this: 1. Do I need to create a concrete class implementing each combination of traits (e.x. implement a CountedWeightedFeature, CountedIndexedfeature and so on) ,or is there some way to avoid it. If I will move to more decorations it will be impossible to mantain classes for all combinations. 2. I want to design a function that weights features based on their count. It's signature should look something like :

def computeWeightsByCount(features :List[T <: Feature with Counted]) :List[T with Weighted] = {...}

T here may be Indexed or not, so this function should have some way to take a class and instansiate a new class that has all the traits of the original class stacked inside plus an additional one.

Is there some elegant way to do this in Scala, or should I totaly rethink this design?

Was it helpful?

Solution

The design looks fine to me, except extending case classes is not recommended. A brief summary of the reasons why can be found here: https://stackoverflow.com/a/12705634/2186890

So you will might want to rewrite Feature as something like this:

trait Feature { def value: String }

Now you can define case classes for pattern matching etc. like this:

case class CountedFeature(value: String, count: Long) extends Feature with Counted

There is no easy way to avoid combinatorial explosion of case classes like this, but you are enabled to use types such as Feature with Counted wherever you like. Keep in mind that you can easily create objects that match type Feature with Counted on the fly. For instance:

val x: Feature with Counted = new Feature with Counted { val value = ""; val count = 0L }

Implementing computeWeightsByCount like you want is a little tricky, because there is no easy way to build a T with Weighted without knowing more about type T. But it can be done with implicit methods. Essentially, we need to have a defined path for generating a T with Weighted from a T for every Feature with Counted that you want to apply this method to. For instance, we start with this:

trait Feature { def value: String }
trait Counted { def count: Long }
trait Weighted { def weight: Double }
trait Indexed { def index: Int }

We want to define computeWeightsByCount like you did in your question, but also taking an implicit method that takes a T and a weight, and produces a T with Weighted:

def computeWeightsByCount[
  T <: Feature with Counted](                                                                                       
  features: List[T])(
  implicit weighted: (T, Double) => T with Weighted
): List[T with Weighted] = {  
  def weight(fc: Feature with Counted): Double = 0.0d
  features map { f => weighted(f, weight(f)) }
}

Now we need to define an implicit method to produce weighted features from the input features. Let's start with getting a Feature with Counted with Weighted from a Feature with Counted. We'll put it in companion object for Feature:

object Feature {
  implicit def weight(fc: Feature with Counted, weight: Double): Feature with Counted with Weighted = {
    case class FCW(value: String, count: Long, weight: Double) extends Feature with Counted with Weighted
    FCW(fc.value, fc.count, weight)
  }
}

We can use it like so:

case class FC(value: String, count: Long) extends Feature with Counted
val fcs: List[Feature with Counted] = List(FC("0", 0L), FC("1", 1L))
val fcws: List[Feature with Counted with Weighted] = computeWeightsByCount[Feature with Counted](fcs)

For any type that you want to compute weighted counts for, you need to define a similar such implicit method.

Admittedly, this is far from a beautiful solution. So yes, you are right, you may want to rethink the design. The advantage to this approach, however, is that any further extensions to the Feature "hierarchy" can be made without needing to make any changes to computeWeightsByCount. Whoever writes the new trait can provide the appropriate implicit methods as well.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top