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

发布于 2024-12-11 06:08:49 字数 618 浏览 0 评论 0原文

依赖方法类型以前是一个实验性功能,现在已默认启用trunk,显然这似乎已经创建了 Scala 社区中的一些兴奋

乍一看,这有什么用处并不是很明显。 Heiko Seeberger 在此处发布了一个依赖方法类型的简单示例,可以在评论中看到可以使用方法上的类型参数轻松重现。所以这不是一个非常有说服力的例子。 (我可能遗漏了一些明显的东西。如果是这样,请纠正我。)

依赖方法类型的用例有哪些实际且有用的示例,它们明显优于替代方法?

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

与现有的类型系统功能相比,他们给我们带来了什么?

另外,依赖方法类型是否与其他高级类型语言(例如 Haskell、OCaml)的类型系统中发现的任何功能类似或从中汲取灵感?

Dependent method types, which used to be an experimental feature before, has now been enabled by default in the trunk, and apparently this seems to have created some excitement in the Scala community.

At first look, it's not immediately obvious what this could be useful for. Heiko Seeberger posted a simple example of dependent method types here, which as can be seen in the comment there can easily be reproduced with type parameters on methods. So that wasn't a very compelling example. (I might be missing something obvious. Please correct me if so.)

What are some practical and useful examples of use cases for dependent method types where they are clearly advantageous over the alternatives?

What interesting things can we do with them that weren't possible/easy before?

What do they buy us over the existing type system features?

Also, are dependent method types analogous to or drawing inspiration from any features found in the type systems of other advanced typed languages such as Haskell, OCaml?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

初雪 2024-12-18 06:08:49

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

那么问题出在哪里呢? 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/文件是进而由NetworkFileManager/RemoteFile细化)。这是一个玩具示例,但该模式是真实的:它在整个 Scala 编译器中使用,并在 Scala Eclipse 插件中广泛使用。

这是使用抽象的示例,

val nfm = new NetworkFileManager
val rf : nfm.Resource = nfm.create
nfm.testHash(rf)
nfm.testDuplicates(rf)

请注意,路径依赖意味着编译器将保证 NetworkFileManager 上的 testHashtestDuplicates 方法可以只能使用与其对应的参数来调用,即。它是自己的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)

经过一段时间的挣扎后,您可能会发现为什么我(或者可能是大卫·麦基弗,我们不记得我们中谁创造了这个术语)将其称为“末日面包店”。

编辑:共识是《Bakery of Doom》是 David MacIver 的创造...

额外的好处是:Scala 的依赖类型形式(以及作为其中一部分的依赖方法类型)受到了编程语言的启发Beta ...它们自然地源于 Beta 一致的嵌套语义。我不知道有任何其他甚至微弱的主流编程语言具有这种形式的依赖类型。 Coq、Cayenne、Epigram 和 Agda 等语言具有不同形式的依赖类型,这种形式在某些方面更为通用,但由于属于类型系统的一部分而存在显着差异,与 Scala 不同,类型系统没有子类型。

More or less any use of member (ie. nested) types can give rise to a need for dependent method types. In particular, I maintain that without dependent method types the classic cake pattern is closer to being an anti-pattern.

So what's the problem? Nested types in Scala are dependent on their enclosing instance. Consequently, in the absence of dependent method types, attempts to use them outside of that instance can be frustratingly difficult. This can turn designs which initially seem elegant and appealing into monstrosities which are nightmarishly rigid and difficult to refactor.

I'll illustrate that with an exercise I give during my Advanced Scala training course,

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
}

It's an example of the classic cake pattern: we have a family of abstractions which are gradually refined through a heirarchy (ResourceManager/Resource are refined by FileManager/File which are in turn refined by NetworkFileManager/RemoteFile). It's a toy example, but the pattern is real: it's used throughout the Scala compiler and was used extensively in the Scala Eclipse plugin.

Here's an example of the abstraction in use,

val nfm = new NetworkFileManager
val rf : nfm.Resource = nfm.create
nfm.testHash(rf)
nfm.testDuplicates(rf)

Note that the path dependency means that the compiler will guarantee that the testHash and testDuplicates methods on NetworkFileManager can only be called with arguments which correspond to it, ie. it's own RemoteFiles, and nothing else.

That's undeniably a desirable property, but suppose we wanted to move this test code to a different source file? With dependent method types it's trivially easy to redefine those methods outside the ResourceManager hierarchy,

def testHash4(rm : ResourceManager)(r : rm.Resource) = 
  assert(r.hash == "9e47088d")

def testDuplicates4(rm : ResourceManager)(r : rm.Resource) = 
  assert(r.duplicates(r))

Note the uses of dependent method types here: the type of the second argument (rm.Resource) depends on the value of the first argument (rm).

It is possible to do this without dependent method types, but it's extremely awkward and the mechanism is quite unintuitive: I've been teaching this course for nearly two years now, and in that time, noone has come up with a working solution unprompted.

Try it for yourself ...

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

After a short while struggling with it you'll probably discover why I (or maybe it was David MacIver, we can't remember which of us coined the term) call this the Bakery of Doom.

Edit: consensus is that Bakery of Doom was David MacIver's coinage ...

For the bonus: Scala's form of dependent types in general (and dependent method types as a part of it) was inspired by the programming language Beta ... they arise naturally from Beta's consistent nesting semantics. I don't know of any other even faintly mainstream programming language which has dependent types in this form. Languages like Coq, Cayenne, Epigram and Agda have a different form of dependent typing which is in some ways more general, but which differs significantly by being part of type systems which, unlike Scala, don't have subtyping.

得不到的就毁灭 2024-12-18 06:08:49
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 中的依赖类型编程 来了解一下。)我想我以前没有见过这样的类比。

trait Graph {
  type Node
  type Edge
  def end1(e: Edge): Node
  def end2(e: Edge): Node
  def nodes: Set[Node]
  def edges: Set[Edge]
}

Somewhere else we can statically guarantee that we aren't mixing up nodes from two different graphs, e.g.:

def shortestPath(g: Graph)(n1: g.Node, n2: g.Node) = ... 

Of course, this already worked if defined inside Graph, but say we can't modify Graph and are writing a "pimp my library" extension for it.

About the second question: types enabled by this feature are far weaker than complete dependent types (See Dependently Typed Programming in Agda for a flavor of that.) I don't think I've seen an analogy before.

蓝眼睛不忧郁 2024-12-18 06:08:49

具体使用抽象类型成员而不是类型时,需要这个新功能参数。当使用类型参数时,家族多态性类型依赖可以是在最新版本和一些旧版本的 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, "")
         ^

This new feature is needed when concrete abstract type members are used instead of type parameters. When type parameters are used, the family polymorphism type dependency can be expressed in the latest and some older versions of Scala, as in the following simplified example.

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, "")
         ^
万劫不复 2024-12-18 06:08:49

我正在开发一个模型,用于与声明式编程形式进行交互环境状态。此处的详细信息不相关(例如,有关回调的详细信息以及与序列化器相结合的 Actor 模型的概念相似性)。

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

环境类必须有一个重载方法,该方法输入要调用的函数,并输入该函数参数的哈希键。因此,此方法可以使用哈希映射中的必要值来调用函数,而无需提供对这些值的公共读取访问权限(因此根据需要,拒绝函数从环境中读取值的能力)。

但是,如果这些哈希键是字符串或整数哈希值,则哈希映射元素类型的静态类型 包含到 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

I'm developing a model for the interoption of a form of declarative programming with environmental state. The details aren't relevant here (e.g. details about callbacks and conceptual similarity to the Actor model combined with a Serializer).

The relevant issue is state values are stored in a hash map and referenced by a hash key value. Functions input immutable arguments that are values from the environment, may call other such functions, and write state to the environment. But functions are not allowed to read values from the environment (so the internal code of the function is not dependent on the order of state changes and thus remains declarative in that sense). How to type this in Scala?

The environment class must have an overloaded method which inputs such a function to call, and inputs the hash keys of the arguments of the function. Thus this method can call the function with the necessary values from the hash map, without providing public read access to the values (thus as required, denying functions the ability to read values from the environment).

But if these hash keys are strings or integer hash values, the static typing of the hash map element type subsumes to Any or AnyRef (hash map code not shown below), and thus a run-time mismatch could occur, i.e. it would be possible to put any type of value in a hash map for a given hash key.

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
}

Although I didn't test the following, in theory I can get the hash keys from class names at runtime employing classOf, so a hash key is a class name instead of a string (using Scala's backticks to embed a string in a class name).

trait DependentHashKey {
  type ValueType
}
trait `the hash key string` extends DependentHashKey {
  type ValueType <: SomeType
}

So static type safety is achieved.

def callit[A](arg1key: DependentHashKey)(func: Env => arg1key.ValueType => A): A
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文