【问题标题】:In the absence of mutable types, is there a case for invariant type parameters?在没有可变类型的情况下,是否存在不变类型参数的情况?
【发布时间】:2020-03-26 03:10:29
【问题描述】:

Java 数组不是完全类型安全的,因为它们是协变的:ArrayStoreException 可以出现在别名数组上。另一方面,Java 集合的类型参数是不变的:例如,List<Thread> 不是List<Runnable> 的子类型(这可能有点违反直觉)。 动机似乎与 Lists 和其他集合是可变有关,因此为了保持类型系统的健全,它们的类型参数必须是不变的。

如果编程语言只支持不可变类型,那么类型参数是协变或逆变(但绝不是不变)的类型系统可以工作吗?换句话说,要使用 Scala 表达方差的方式,可以使用 List[+E]Function[-T, +R]Map[+K, +V] 等。 我知道有一些较旧的语言(例如GNU Sather)似乎只支持协变/逆变参数类型。

我的一般问题是:在一个完全不可变的数据类型的世界中,是否存在一种特别需要 invariant 参数类型(而不是协变或逆变)的情况?是否有一些不可变数据结构的示例,只有使用不变类型参数才能正确?

【问题讨论】:

  • 我很惊讶你写了Map[-K, +V];当然应该是Map[+K, +V]? (不能在不中断迭代的情况下允许 -K。但 +K 会很好,因为缺少键会隐式映射到 null。)
  • 唯一自以为是的部分是第一句话半。如果您摆脱有关 Java 列表的东西,一般问题会很有趣且没有意见。
  • 回复:“[我的假设]之一是没有空指针,所以我想到的这个虚构版本的 Map 将返回 Optional 或 union 或 sum 类型,如 'V|什么都没有'":好的,但这与此目的相同。回复:“映射基本上是一个用于获取键值的函数,这就是为什么我对 Map 使用与函数相同的类型参数差异”:映射有点像 partial 函数,因为它的键集是其键类型实例集的子集。它通过将缺失的键映射到默认值来弥补这一点,[continued]
  • [continued] 这意味着允许检查密钥类型错误的映射是可以的。回复:“根本不会有像 Iterator 这样的东西,因为它依赖于可变的内部状态”:我认为迭代是集合的基本特征;幸运的是,您可以使用不可变的迭代器(例如,使用 headtail 方法,后者返回新的迭代器)。
  • List<Thread> 不是List<Runnable> 的子类型与可变性没有太大关系,尽管该问题仅通过可变结构表现出来——不可变结构是一种极端情况。乍一看,没有任何想法,它不是子类型似乎违反直觉,但是当您考虑作为子类型的含义意味着您可以将子类型分配给超类型的变量时,应该很明显为什么不是。

标签: java generics types immutability covariance


【解决方案1】:

所以,每个类型系统要么允许一些不健全的程序,要么禁止一些健全的程序,或者两者兼而有之(这是Rice's theorem 的结果),所以一个很好的工作假设是,是的,你提出的任何限制都必然会统治淘汰一些原本会被允许的声音节目。另一方面,人类是无限聪明的,所以从另一个意义上说,答案是否定的:如果你像你描述的那样添加一个限制,那没关系,人们会在需要时想办法绕过它。 (当然,有时他们会提出您不喜欢的解决方法,例如放弃您的语言。)

但我认为您真正需要的是一个令人信服的案例:一个现实的例子,在直接支持该示例和坚持您的提议之间选择,要求所有类型参数都是无论是协变还是逆变,您的直觉都会告诉您放弃该提案,以便您可以直接支持该示例。

由于您已经确定了类型参数不能协变的各种情况以及类型参数不能逆变的各种情况(例如,Function[-T, +R] 很好,但反过来会完全不合理),一个好的方法是搜索使用相同类型参数的情况两次,一次以不能协变的方式,一次以不能协变的方式逆变的。一个简单的例子是 UnaryOperator[T] <: function t java java.util.function.unaryoperator>,它的 'apply' 方法返回与其接受的相同类型。 UnaryOperator[String] 不能用作 UnaryOperator[Object](因为您不能将任意对象传递给它),但 UnaryOperator[Object] 也不能用作 UnaryOperator[String](因为即使你传递一个字符串,它也可能返回一些不同的对象)。

举一个更充实的现实例子。 . .想象一个二叉搜索树 TreeMap[K, +V] <: map v java java.util.treemap>。大概我们想要支持诸如'firstKey'和'floorEntry'和'iterator'等方法(或者至少,其中一些),所以我们不能使K逆变:TreeMap[Object, Foo]可以' t 用作 TreeMap[String, Foo],因为当我们检索一个键时,该键可能不是一个字符串。

而且由于它是一个二叉搜索树,它在内部需要一个 Comparator[K],这立即使得 K 是协变的变得很棘手:如果你使用 TreeMap[String, Foo] 作为 TreeMap[Object, Foo],那么你隐式使用 Comparator[String] 作为 Comparator[Object],这是行不通的。现在,由于地图肯定只包含字符串键,也许'get'方法可以通过在使用 Comparator[String] 调用之前预先检查键的类型来解决这个问题;但是 'floorEntry' 和 'ceilingEntry' 方法仍然是一个问题:什么条目出现在无法与地图中的键进行比较的任意对象“之前”或“之后”?

即使您已经说过您的地图是不可变的,您可能仍然需要某种“放置”方法,只是一种返回修改后的地图副本的纯函数式方法。 (纯函数式红黑树支持与可变树相同的不变量和最坏情况的渐近时间复杂度,因此除了类型系统,这当然是合理的做法。)但是如果可以将 TreeMap[String, Foo] 用作TreeMap[Object, Foo],那么它的 'put' 方法需要支持返回一个包含 non-String 键的二叉搜索树——即使它的 Comparator[String] 没有为这样的键。

(在评论中,您提到 Scala 实际上使用不变键类型定义了 Map[K, +V]。我从未使用过 Scala,但我敢打赌这正是原因。)

【讨论】:

  • 谢谢@ruakh,非常全面的回答!尤其是您的UnaryOperator[T]/Function[T,T] 示例以及您描述的Comparator[K] 和'floorEntry/ceilingEntry` 方法的问题正是我正在寻找的反例!
猜你喜欢
  • 2022-10-13
  • 2023-03-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多