Domanda

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.

È stato utile?

Soluzione

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.

Altri suggerimenti

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.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top