依赖方法类型有哪些引人入胜的使用场景?

131

依赖方法类型曾经是一个实验性功能,现在已经默认启用,显然这在Scala社区引起了一些兴奋

乍一看,这个功能的用处并不是很明显。Heiko Seeberger在这里发布了一个简单的例子,可以看到评论中使用方法类型参数也可以轻松地重现该例子。所以那不是一个非常有说服力的例子。(如果我错了,请纠正我。)

有哪些实际和有用的例子可以使用依赖方法类型,其中它们明显优于其他选择?

我们可以用它们做些什么以前不可能/不容易做到的有趣的事情?

相对于现有的类型系统功能,它们为我们带来了什么好处?

此外,依赖方法类型是否类似于或受到其他高级类型语言(如Haskell、OCaml)中发现的某些功能的启发?


你可能会对浏览http://www.haskell.org/haskellwiki/Dependent_type感兴趣。 - Dan Burton
谢谢链接,丹!我对依赖类型有一般的了解,但是依赖方法类型的概念对我来说还比较新。 - missingfaktor
在我看来,“依赖方法类型”只是依赖于一个或多个方法输入类型(包括调用该方法的对象类型)的类型;除了依赖类型的一般概念外,没有什么特别的。也许我漏掉了什么? - Dan Burton
不,你没有,但显然我做了。 :-) 我之前没有看到两者之间的联系。现在它非常清晰了。 - missingfaktor
4个回答

115

几乎任何使用成员类型(即嵌套类型)的情况都可能需要使用相关的方法类型。特别地,我认为如果没有相关的方法类型,经典的 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/ResourceFileManager/File 细化,然后再被 NetworkFileManager/RemoteFile 细化)。这只是一个玩具示例,但蛋糕模式确实存在:它在Scala编译器中得到广泛应用,并且在Scala Eclipse插件中也得到了广泛使用。

以下是该抽象化概念的使用示例:

val nfm = new NetworkFileManager
val rf : nfm.Resource = nfm.create
nfm.testHash(rf)
nfm.testDuplicates(rf)
请注意路径依赖性意味着编译器将保证只有针对NetworkFileManager自己的RemoteFiles才能作为参数调用testHashtestDuplicates方法,而不是其他任何东西。
这无疑是一种理想的属性,但是如果我们想将此测试代码移到另一个源文件中怎么办?使用依赖方法类型轻松地在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那样拥有子类型。


2
这个术语是由David MacIver创造的,但无论如何,它非常具有描述性。这是为什么依赖方法类型如此令人兴奋的绝妙解释。干得好! - Daniel Spiewak
这个话题最初是在我们很久以前的 #scala 对话中提出来的...就像我说的,我记不清是我们谁先说的了。 - Miles Sabin
似乎我的记忆欺骗了我……大家一致认为这是David MacIver提出的说法。 - Miles Sabin
是的,当时我不在(#scala),但Jorge在那里,那就是我获取信息的地方。 - Daniel Spiewak
利用抽象类型成员细化,我能够相当轻松地实现testHash4函数。 def testHash4[R <: ResourceManager#BasicResource](rm:ResourceManager {type Resource = R},r:R)= assert(r.hash ==“ 9e47088d”) 虽然我认为这可以被视为依赖类型的另一种形式。 - Marco van Hilst

53
trait 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中进行依赖型编程)。我之前没有看到过类似的比喻。


6

当使用具体的抽象类型成员代替类型参数时,需要这个新功能。当使用类型参数时,在最新版本和一些较旧的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
无论如何,这与依赖方法类型无关。以Alexey的例子为例(https://dev59.com/yGsz5IYBdhLWcg3wfn66#7860821)。使用您的方法(包括我评论的改进版本)无法实现目标。它将确保n1.Node =:= n2.Node,但它不会确保它们都在同一个图中。如果我理解正确,DMT确保了这一点。 - nafg
谢谢您的指出,@nafg。我已经添加了“具体”的词语以澄清我不是在提及类型成员的细化案例。就我所见,这仍然是依赖方法类型的一个有效用例,尽管在其他用例中它们可能具有更多的能力。或者是我错过了您第二条评论的基本要点? - Shelby Moore III

3

我正在开发一种模型,将声明式编程形式与环境状态相结合。这里的细节不是很相关(例如有关回调和概念上类似于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
“哈希映射元素类型的静态类型包含 Any 或 AnyRef” - 我不明白。当您说元素类型时,是指键类型还是值类型(即 HashMap 的第一个或第二个类型参数)?为什么会被包含? - Robin Green
哈希表中值的类型。据我所知,这是因为Scala中集合不允许放置多种类型,除非你将它们归入共同超类型之下,因为Scala没有联合(或分离)类型。请参阅我的有关Scala中子sume的问答。 - Shelby Moore III

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接