【问题标题】:How to implement type-safe extensible type system in Scala?如何在 Scala 中实现类型安全的可扩展类型系统?
【发布时间】:2012-12-30 05:51:29
【问题描述】:

我正在用 Scala 编写一个项目。该项目涉及一组功能和一组配置,两者都是可扩展的。 “可扩展”是指我稍后将在层次结构中添加新功能,并且它们必须与任何配置一起使用而无需重新编译。这是下图的特征层次结构:

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")
    }
}

配置基本上只是为Board 定义了很多参数,包括每个Feature 的初始特征计数。在运行时创建配置有几种可能的策略:预定义的、随机的、使用用户提供的值等。理论上,我希望能够编写这样的东西(不是有效的 Scala 代码!):

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) { ... }

显然,它无法编译。我的问题是:在 Scala 中表示这个可扩展系统的最佳方式是什么?我总是可以在Config 中包含类似Map[Class, Int] 的内容,但它不是类型安全的:程序员可以在Map 中插入不是Features 的类。那么,在 Scala 类型系统中是否有一种方法可以表示 Map[Class[T &lt;: Feature], Int] 之类的东西,其中 Map 中的不同键可能具有不同的 Feature 子类型?或者,也许有某种方法可以将所有这些行为移至Feature 层次结构?

谢谢。

【问题讨论】:

  • 嗯...是否存在类型不安全的类型系统?

标签: scala types extensibility


【解决方案1】:

您可以使用ClassManifest(Scala 2.10 中的ClassTag)来改进您的地图解决方案:

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
        )
    }
}

每次调用 featureCount 时,编译器都会为您在括号中传递的类型使用正确的 classManifest。 erasure 方法返回对应的类。

备注:避免使用抽象值,它有烦人的效果,并且会破坏二进制兼容性。

【讨论】:

  • 不幸的是,它无法编译:man.erasure 返回Class[_],但featureMap.apply 需要Class[_ &lt;: Feature]
  • @Skiminok 您可以移除地图的类型约束,因为它已经在featureCount 中表示。如果要确保插入的值是Feature 的子类,可以用返回相应类清单擦除的方法替换classOf。如果不够清楚,我可以举个例子。
【解决方案2】:

我找到了基于Map 的解决方案(尽管它可能不是最优雅的解决方案):

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
        )
    }
}

【讨论】:

    【解决方案3】:

    您确定需要地图来了解每个键的Feature 的特定子类型吗?例如,我会这样写:

    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)
    

    现在您可以只写featureCount(Foo)featureCount.get(Bar) 等。如果您想确保没有非对象值可以潜入,您甚至可以将映射输入为Map[Feature with Singleton, Int]

    或者,如果您真的想要一个以类型为键的映射,您可以使用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))
    }
    

    然后:

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

    不过,我不确定这对你有什么好处。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-09-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-09-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多