几乎任何使用成员类型(即嵌套类型)的情况都可能需要使用相关的方法类型。特别地,我认为如果没有相关的方法类型,经典的 Cake 模式更接近于反模式。
问题在哪里?在 Scala 中,嵌套类型依赖于其封闭实例。因此,在没有相关的方法类型的情况下,试图在该实例之外使用它们可能会非常困难。这可能会使最初看起来优雅和吸引人的设计变成丑陋的怪物,难以重构。
我将通过一项练习来说明这一点,这是我在我的高级 Scala 培训课程中提供的。
trait ResourceManager {
type Resource <: BasicResource
trait BasicResource {
def hash : String
def duplicates(r : Resource) : Boolean
}
def create : Resource
// Test methods: exercise is to move them outside ResourceManager
def testHash(r : Resource) = assert(r.hash == "9e47088d")
def testDuplicates(r : Resource) = assert(r.duplicates(r))
}
trait FileManager extends ResourceManager {
type Resource <: File
trait File extends BasicResource {
def local : Boolean
}
override def create : Resource
}
class NetworkFileManager extends FileManager {
type Resource = RemoteFile
class RemoteFile extends File {
def local = false
def hash = "9e47088d"
def duplicates(r : Resource) = (local == r.local) && (hash == r.hash)
}
override def create : Resource = new RemoteFile
}
这是经典蛋糕模式的一个示例:我们有一组抽象化概念,通过层次结构逐渐细化(ResourceManager
/Resource
被 FileManager
/File
细化,然后再被 NetworkFileManager
/RemoteFile
细化)。这只是一个玩具示例,但蛋糕模式确实存在:它在Scala编译器中得到广泛应用,并且在Scala Eclipse插件中也得到了广泛使用。
以下是该抽象化概念的使用示例:
val nfm = new NetworkFileManager
val rf : nfm.Resource = nfm.create
nfm.testHash(rf)
nfm.testDuplicates(rf)
请注意路径依赖性意味着编译器将保证只有针对NetworkFileManager
自己的RemoteFiles
才能作为参数调用testHash
和testDuplicates
方法,而不是其他任何东西。ResourceManager
层次结构之外重新定义这些方法。def testHash4(rm : ResourceManager)(r : rm.Resource) =
assert(r.hash == "9e47088d")
def testDuplicates4(rm : ResourceManager)(r : rm.Resource) =
assert(r.duplicates(r))
注意此处使用的依赖方法类型:第二个参数的类型(rm.Resource
)取决于第一个参数(rm
)的值。
没有依赖方法类型也可以做到这一点,但是非常笨拙,并且机制相当不直观:我已经教授了这门课将近两年了,在这段时间里,没有人在无提示的情况下想出了可行的解决方案。
你可以自己试试看......
// Reimplement the testHash and testDuplicates methods outside
// the ResourceManager hierarchy without using dependent method types
def testHash // TODO ...
def testDuplicates // TODO ...
testHash(rf)
testDuplicates(rf)
你可能会在一番挣扎后发现,为什么我(或者也许是David MacIver,我们记不清是谁创造了这个术语)称之为“毁灭面包店”。
编辑:一致认为“毁灭面包店”是David MacIver的创造...
额外奖励:Scala中的依赖类型(以及其中的依赖方法类型)的形式一般受到编程语言Beta的启发...它们自然地出现在Beta的一致嵌套语义中。我不知道还有其他任何稍微主流的编程语言具有这种形式的依赖类型。像Coq、Cayenne、Epigram和Agda这样的语言具有不同形式的依赖类型,某些方面上更加通用,但与Scala不同的是,它们是类型系统的一部分,这些类型系统不像Scala那样拥有子类型。
def testHash4[R <: ResourceManager#BasicResource](rm:ResourceManager {type Resource = R},r:R)= assert(r.hash ==“ 9e47088d”)
虽然我认为这可以被视为依赖类型的另一种形式。 - Marco van Hilsttrait Graph {
type Node
type Edge
def end1(e: Edge): Node
def end2(e: Edge): Node
def nodes: Set[Node]
def edges: Set[Edge]
}
在其他地方,我们可以静态地保证我们没有混淆来自两个不同图形的节点,例如:
def shortestPath(g: Graph)(n1: g.Node, n2: g.Node) = ...
当然,如果定义在Graph
内部,这已经可以工作了,但是假设我们无法修改Graph
并且正在为其编写“pimp my library”扩展。
关于第二个问题:此功能启用的类型比完全依赖类型要弱得多(有关该主题的一些味道,请参见Agda中进行依赖型编程)。我之前没有看到过类似的比喻。
当使用具体的抽象类型成员代替类型参数时,需要这个新功能。当使用类型参数时,在最新版本和一些较旧的Scala版本中可以表达家族多态类型依赖关系,如下面简化的示例所示。
trait C[A]
def f[M](a: C[M], b: M) = b
class C1 extends C[Int]
class C2 extends C[String]
f(new C1, 0)
res0: Int = 0
f(new C2, "")
res1: java.lang.String =
f(new C1, "")
error: type mismatch;
found : C1
required: C[Any]
f(new C1, "")
^
trait C {type A}; def f[M](a: C { type A = M}, b: M) = 0;class CI extends C{type A=Int};class CS extends C{type A=String}
等等。 - nafg我正在开发一种模型,将声明式编程形式与环境状态相结合。这里的细节不是很相关(例如有关回调和概念上类似于Actor模型与序列化器的详细信息)。
相关问题在于状态值存储在哈希映射中,并由哈希键值引用。函数输入不可变参数,即来自环境的值,可以调用其他这样的函数,并将状态写入环境。但是,函数不被允许读取环境中的值(因此函数的内部代码不依赖于状态更改的顺序,从而在这个意义上保持了声明性)。如何在Scala中进行类型设置?
环境类必须有一个重载方法,该方法输入要调用的这样一个函数以及函数参数的哈希键。因此,该方法可以使用哈希映射中必要的值调用函数,而不提供对这些值的公共读取访问(因此按照要求,禁止函数读取环境中的值)。
但是如果这些哈希键是字符串或整数哈希值,则哈希映射元素类型的静态类型subsumes为Any或AnyRef(未在下面显示哈希映射代码),因此可能会发生运行时不匹配,即可以将任何类型的值放入给定哈希键的哈希映射中。
trait Env {
...
def callit[A](func: Env => Any => A, arg1key: String): A
def callit[A](func: Env => Any => Any => A, arg1key: String, arg2key: String): A
}
classOf
从类名中获取哈希键,因此哈希键是类名而不是字符串(使用Scala的反引号将字符串嵌入类名中)。trait DependentHashKey {
type ValueType
}
trait `the hash key string` extends DependentHashKey {
type ValueType <: SomeType
}
因此,静态类型安全得以实现。
def callit[A](arg1key: DependentHashKey)(func: Env => arg1key.ValueType => A): A
def callit[A](argkeys: Tuple[DependentHashKey,DependentHashKey])(func: Env => argkeys._0.ValueType => argkeys._1.ValueType => A): A
。我们不会使用参数键的集合,因为元素类型将被包含在集合类型中(在编译时不知道)。 - Shelby Moore III