Pregunta

I'm writing a project in Scala. This project involves a set of features and a set of configurations, both extensible. By "extensible" I mean that I'm going to add new features in the hierarchy later, and they have to work with any configuration without recompilation. Here's the feature hierarchy for the following illustration:

trait Feature {
    def apply(board: Board)
}

class Foo extends Feature {
    def apply(board: Board) {
        println(board formatted "Foo: %s")
    }
}

class Bar extends Feature {
    def apply(board: Board) {
        println(board formatted "Bar: %s")
    }
}

A configuration basically just defines a lot of parameters for a Board, including initial feature count for each Feature. There are several possible strategies to create a configuration at run-time: predefined, random, with user-provided values, etc. In theory, I want to be able to write something like this (not a valid Scala code!):

abstract class Config(val param: Int) {
    val ConfigParameter: Int
    def featureCount[T <: Feature]: Int
}

object Config {
    def makeBasic(param: Int) = new Config(param) {
        val ConfigParameter = param
        def featureCount[Foo] = 3
        def featureCount[Bar] = 7
    }
    def makeRandom(param: Int) = new Config(param) { ... }
    def makeWithUserValues(param: Int, ...) = new Config(param) { ... }
    def makeByStandardISO1234567(param: Int) = new Config(param) { ... }
}

class Board(val config: Config) { ... }

Obviously, it doesn't compile. My question is: what's the best way to represent this extensible system in Scala? I can always include something like Map[Class, Int] in the Config, but it isn't type-safe: a programmer can insert classes in such Map that aren't Features. So, is there a way in the Scala type system to represent something like Map[Class[T <: Feature], Int] where different keys in the Map could be of different Feature subtypes? Alternatively, maybe there's some way to move all this behavior to the Feature hierarchy?

Thank you.

¿Fue útil?

Solución

You can use ClassManifest (ClassTag in Scala 2.10) to improve your map solution:

package object features {
  type FeatureMap = Map[Class[_ <: Feature], Int]
}

abstract class Config(val param: Int) {
    def ConfigParameter: Int
    def featureMap: FeatureMap
    def featureCount[T<:Feature]( implicit man: ClassManifest[T] ): Int = 
      featureMap( man.erasure )
}

object Config {
    def makeBasic(param: Int) = new Config(param) {
        val ConfigParameter = param
        lazy val featureMap: FeatureMap = Map(
            classOf[Foo] -> 3,
            classOf[Bar] -> 7
        )
    }
}

Each time you call featureCount the compiler will use the right classManifest for the type you passed between brackets. The erasure method returns the coresponding class.

Remark: Avoid abstract vals, it has annoying effects and can break binary compatibility.

Otros consejos

I found the Map-based solution (although maybe it's not the most elegant one):

package object features {
    type FeatureMap = Map[Class[_ <: Feature], Int]
}

abstract class Config(val param: Int) {
    val ConfigParameter: Int
    val FeatureCount: FeatureMap
}

object Config {
    def makeBasic(param: Int) = new Config(param) {
        val ConfigParameter = param
        lazy val FeatureCount: FeatureMap = Map(
            classOf[Foo] -> 3,
            classOf[Bar] -> 7
        )
    }
}

Are you sure you need the map to know about each key's specific subtype of Feature? For example, here's how I'd write this:

trait Feature { def apply(board: Board) }

case object Foo extends Feature {
  def apply(board: Board) { printf("Foo: %s", board) }
}

case object Bar extends Feature {
  def apply(board: Board) { printf("Bar: %s", board) }
}

// ...
val featureCount: Map[Feature, Int] = Map(Foo -> 3, Bar -> 7)

Now you can just write featureCount(Foo), featureCount.get(Bar), etc. You could even type the map as Map[Feature with Singleton, Int] if you wanted to be sure that no non-object values can sneak in.

Alternatively, if you really wanted a map keyed on types, you could do something like this with Shapeless:

trait Feature

case class Foo(count: Int) extends Feature
case class Bar(count: Int) extends Feature

import shapeless._

object featureWithCount extends Poly0 {
  implicit def foo = at(Foo(3))
  implicit def bar = at(Bar(7))
}

And then:

scala> featureWithCount[Foo]
res0: Foo = Foo(3)

scala> featureWithCount[Bar]
res1: Bar = Bar(7)

I'm not sure that buys you much here, though.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top