依赖方法类型有哪些引人注目的用例?


127

依赖的方法类型以前曾经是实验性功能,现在默认情况下已在主干中启用,显然,这似乎在Scala社区中引起了一定的兴趣

乍一看,这可能会产生什么效果尚不清楚。Heiko Seeberger在此处发布了一个简单的依赖方法类型的示例,从注释中可以看出,可以很容易地在方法上使用类型参数来复制它们。因此,这不是一个非常引人注目的例子。(我可能缺少明显的东西。如果是,请更正我。)

在依赖方法类型明显优于替代方法的情况下,有哪些实用且有用的用例示例?

我们可以对他们做哪些以前不可能/不容易做的有趣事情?

他们通过现有的类型系统功能向我们购买什么?

此外,从属方法类型是否类似于在其他高级键入语言(例如Haskell,OCaml)的类型系统中找到的任何功能或从中获得启发?



感谢您的链接,丹!我一般都知道依赖类型,但是依赖方法类型的概念对我来说相对较新。
missingfaktor 2011年

在我看来,“从属方法类型”只是依赖于一种或多种方法输入类型(包括在其上调用该方法的对象的类型)的类型。除了依赖类型的一般概念外,没有什么比这更疯狂的了。也许我缺少什么?
丹·伯顿

不,您没有,但显然我做到了。:-)我之前没有看到两者之间的联系。现在,它非常清晰。
missingfaktor 2011年

Answers:


112

成员(即嵌套)类型的任何使用或多或少都会引起对依赖方法类型的需求。特别是,我坚持认为,如果没有依赖的方法类型,那么经典的蛋糕模式更接近于成为反模式。

所以有什么问题?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)

请注意,路径依赖性意味着编译器将保证只能使用与之对应的参数调用on testHashtestDuplicates方法NetworkFileManager,即。它是自己的RemoteFiles,仅此而已。

无疑,这是一个理想的属性,但是假设我们想将此测试代码移至其他源文件?使用相关方法类型,可以很容易地在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)的造币...

值得一提的是:Scala依赖类型的形式(以及依赖方法类型的一部分)通常受到编程语言Beta的启发……它们自然源于Beta一致的嵌套语义。我什至不知道还有其他任何形式的依赖类型的主流编程语言。诸如Coq,Cayenne,Epigram和Agda之类的语言具有不同形式的依存类型,在某些方面更通用,但由于属于类型系统而与Scala不同,后者没有子类型,因此存在显着差异。


2
大卫·麦克弗(David MacIver)创造了这个名词,但无论如何,它都具有描述性。这很好地解释了为什么依赖方法类型如此令人兴奋。干得好!
Daniel Spiewak

最初是在我们两个人之间在#scala上的对话中出现的……就像我说的我不记得是谁首先说的。
Miles Sabin

似乎我的记忆在对我开玩笑...共识是这是David MacIver的造物。
Miles Sabin

是的,我当时不在那儿(在#scala上),但是Jorge在那里,这就是我获取信息的地方。
Daniel Spiewak 2011年

通过使用抽象类型成员细化,我能够非常轻松地实现testHash4函数。def testHash4[R <: ResourceManager#BasicResource](rm: ResourceManager { type Resource = R }, r: R) = assert(r.hash == "9e47088d")我想这可以看作是依赖类型的另一种形式。
Marco van Hilst,2015年

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) = ... 

当然,如果在inside中定义,这已经可以工作Graph,但是说我们不能修改,Graph并且正在为其编写“ pimp my library”扩展。

关于第二个问题:不同类型的支持此功能是远远高于完全依赖类型较弱(见依赖性键入阿格达编程我不认为我见过一个比喻做的一个味道。)。


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为例(stackoverflow.com/a/7860821/333643)。使用您的方法(包括我评论的优化版本)无法实现目标。它将确保n1.Node =:= n2.Node,但不会确保它们都在同一图中。IIUC DMT确实确保了这一点。
2013年

@nafg感谢您指出这一点。我添加了具体一词是为了使我明白,我并不是在指类型成员的优化情况。据我所知,尽管您指出(在我所知道的范围内)它们可以在其他用例中发挥更大的作用,但对于依存方法类型而言,这仍然是一个有效的用例。还是我错过了您第二点评论的基本要点?
谢尔比摩尔三世

3

我正在开发一个模型,用于将声明式编程的形式与环境状态互换。这些细节在这里无关紧要(例如,有关回调和与Actor模型结合了Serializer的概念相似性的细节)。

相关的问题是状态值存储在哈希图中,并由哈希键值引用。函数输入不可变参数,这些参数是来自环境的值,可以调用其他此类函数,并将状态写入环境。但是,不允许函数从环境中读取值(因此,函数的内部代码不依赖于状态更改的顺序,因此在这种意义上仍然是声明性的)。如何在Scala中输入?

环境类必须具有重载方法,该方法输入要调用的函数,并输入该函数的参数的哈希键。因此,此方法可以从哈希图中调用具有必要值的函数,而无需提供对值的公共读取访问(因此,根据需要,拒绝函数从环境中读取值的功能)。

但是,如果这些散列键是字符串或整数的散列值,所述散列映射元素类型的静态类型涵括于任何或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

当我们需要在单个值中传递参数键时,我没有进行测试,但是假设我们可以使用元组,例如2参数重载def callit[A](argkeys: Tuple[DependentHashKey,DependentHashKey])(func: Env => argkeys._0.ValueType => argkeys._1.ValueType => A): A。我们不会使用参数键的集合,因为元素类型将包含在集合类型中(在编译时未知)。
谢尔比·摩尔三世

“哈希映射元素类型的静态类型包含在Any或AnyRef中”-我不遵循。当您说元素类型时,您指的是键类型还是值类型(即HashMap的第一个或第二个类型参数)?为何要包含它?
罗宾·格林

@RobinGreen哈希表中值的类型。Afair,之所以被包含,是因为您不能在Scala的集合中放置多个类型,除非您使用它们的普通超类型,因为Scala没有并集(析取)类型。请参阅我对Scala中包含的问答。
谢尔比·摩尔三世
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.