在没有可变类型的情况下,是否存在不变类型参数的情况?
In the absence of mutable types, is there a case for invariant type parameters?
Java 数组不是完全类型安全的,因为它们是协变的:ArrayStoreException
可以出现在别名数组上。另一方面,Java 集合的类型参数是不变的:例如,List<Thread>
不是 List<Runnable>
的子类型(这可能有点违反直觉)。
动机似乎与 List
s 和其他集合 可变 有关,因此为了保持类型系统的健全,它们的类型参数必须是不变的。
如果编程语言仅支持 不可变 类型,那么类型参数是协变或逆变(但永远不会不变)的类型系统可以工作吗?换句话说,要使用 Scala 的方差表达方式,可以有 List[+E]
、Function[-T, +R]
、Map[+K, +V]
等。
我知道有一些较旧的语言(例如 GNU Sather)似乎只支持 co-/contravariant 参数类型。
我的一般问题是:在一个完全不可变数据类型的世界中,是否存在这样一种情况,即人们特别需要一个 invariant 参数类型(而不是 co- 或逆变)?是否有一些只对不变类型参数正确的不可变数据结构的示例?
所以,每个类型系统要么允许一些不健全的程序,要么禁止一些健全的程序,或者两者兼而有之(这是 Rice's theorem 的结果),所以一个好的工作假设是,是的,你想出的任何限制势必会排除一些原本会被允许的声音节目。另一方面,人类是无限聪明的,所以从另一种意义上说,答案是否定的:如果你像你描述的那样添加一个限制,那没关系,人们会在需要的时候想办法绕过它。 (当然,有时候他们想出的变通办法是你不喜欢的,比如放弃你的语言。)
但我认为您真正要求的是一个 令人信服的 案例:一个现实的例子,在这个例子中,可以选择直接支持该示例还是坚持您的要求要求所有类型参数要么是协变的要么是逆变的,你的直觉会告诉你放弃这个提议,这样你就可以直接支持那个例子。
由于您已经确定了类型参数不能协变的各种情况和类型参数不能逆变的各种情况(例如,Function[-T, +R] 很好,但是反过来是完全不合理的),一个好的方法是搜索使用相同类型参数的情况两次,一次以不能协变的方式和一次以某种方式那不可能是逆变的。一个简单的例子是 UnaryOperator[T] <: Function[T, T],类似于 Java 的 java.util.function.UnaryOperator,其 'apply' 方法 returns与其接受的类型相同。 UnaryOperator[String] 不能用作 UnaryOperator[Object](因为您不能将任意对象传递给它),但 UnaryOperator[Object] 也不能用作 UnaryOperator[String](因为即使你传递给它一个字符串,它也可能 return 一些不同的对象)。
一个更充实的现实例子。 . .想象一个二叉搜索树 TreeMap[K, +V] <: Map[K, V],类似于 Java 的 java.util.TreeMap。大概我们想要支持 'firstKey' 和 'floorEntry' 和 'iterator' 等方法(或者至少,其中一些),所以我们不能使 K 逆变:一个 TreeMap[ Object, Foo] 不能用作 TreeMap[String, Foo],因为当我们检索键时,键可能不是字符串。
并且由于它是一个二叉搜索树,它内部需要一个 Comparator[K],这立即使 K 的协变变得棘手:如果您将 TreeMap[String, Foo] 用作 TreeMap[Object, Foo] ],那么您隐含地使用 Comparator[String] 作为 Comparator[Object],这是行不通的。现在,由于映射肯定只包含字符串键,也许 'get' 方法可以通过在调用之前预先检查键的类型来解决这个问题 using Comparator[String];但是 'floorEntry' 和 'ceilingEntry' 方法仍然是一个问题:什么条目来自 "before" 或 "after" 无法与地图中的键进行比较的任意对象?
即使您说过您的地图是不可变的,您可能仍然需要某种 'put' 方法,只是一种纯功能性方法,return 是地图的修改副本. (纯函数式红黑树支持与可变树相同的不变量和最坏情况渐近时间复杂度,因此撇开类型系统不谈,这当然是一件合理的事情。)但是如果 TreeMap[String, Foo] 可以用作TreeMap[Object, Foo],那么它的 'put' 方法需要支持 returning 包含 non-String 键的二叉搜索树——即使它的 Comparator [String] 未定义此类键的顺序。
(在评论中,您提到 Scala 实际上定义了具有不变键类型的 Map[K, +V]。我从未使用过 Scala,但我敢打赌这就是原因。)
Java 数组不是完全类型安全的,因为它们是协变的:ArrayStoreException
可以出现在别名数组上。另一方面,Java 集合的类型参数是不变的:例如,List<Thread>
不是 List<Runnable>
的子类型(这可能有点违反直觉)。
动机似乎与 List
s 和其他集合 可变 有关,因此为了保持类型系统的健全,它们的类型参数必须是不变的。
如果编程语言仅支持 不可变 类型,那么类型参数是协变或逆变(但永远不会不变)的类型系统可以工作吗?换句话说,要使用 Scala 的方差表达方式,可以有 List[+E]
、Function[-T, +R]
、Map[+K, +V]
等。
我知道有一些较旧的语言(例如 GNU Sather)似乎只支持 co-/contravariant 参数类型。
我的一般问题是:在一个完全不可变数据类型的世界中,是否存在这样一种情况,即人们特别需要一个 invariant 参数类型(而不是 co- 或逆变)?是否有一些只对不变类型参数正确的不可变数据结构的示例?
所以,每个类型系统要么允许一些不健全的程序,要么禁止一些健全的程序,或者两者兼而有之(这是 Rice's theorem 的结果),所以一个好的工作假设是,是的,你想出的任何限制势必会排除一些原本会被允许的声音节目。另一方面,人类是无限聪明的,所以从另一种意义上说,答案是否定的:如果你像你描述的那样添加一个限制,那没关系,人们会在需要的时候想办法绕过它。 (当然,有时候他们想出的变通办法是你不喜欢的,比如放弃你的语言。)
但我认为您真正要求的是一个 令人信服的 案例:一个现实的例子,在这个例子中,可以选择直接支持该示例还是坚持您的要求要求所有类型参数要么是协变的要么是逆变的,你的直觉会告诉你放弃这个提议,这样你就可以直接支持那个例子。
由于您已经确定了类型参数不能协变的各种情况和类型参数不能逆变的各种情况(例如,Function[-T, +R] 很好,但是反过来是完全不合理的),一个好的方法是搜索使用相同类型参数的情况两次,一次以不能协变的方式和一次以某种方式那不可能是逆变的。一个简单的例子是 UnaryOperator[T] <: Function[T, T],类似于 Java 的 java.util.function.UnaryOperator
一个更充实的现实例子。 . .想象一个二叉搜索树 TreeMap[K, +V] <: Map[K, V],类似于 Java 的 java.util.TreeMap
并且由于它是一个二叉搜索树,它内部需要一个 Comparator[K],这立即使 K 的协变变得棘手:如果您将 TreeMap[String, Foo] 用作 TreeMap[Object, Foo] ],那么您隐含地使用 Comparator[String] 作为 Comparator[Object],这是行不通的。现在,由于映射肯定只包含字符串键,也许 'get' 方法可以通过在调用之前预先检查键的类型来解决这个问题 using Comparator[String];但是 'floorEntry' 和 'ceilingEntry' 方法仍然是一个问题:什么条目来自 "before" 或 "after" 无法与地图中的键进行比较的任意对象?
即使您说过您的地图是不可变的,您可能仍然需要某种 'put' 方法,只是一种纯功能性方法,return 是地图的修改副本. (纯函数式红黑树支持与可变树相同的不变量和最坏情况渐近时间复杂度,因此撇开类型系统不谈,这当然是一件合理的事情。)但是如果 TreeMap[String, Foo] 可以用作TreeMap[Object, Foo],那么它的 'put' 方法需要支持 returning 包含 non-String 键的二叉搜索树——即使它的 Comparator [String] 未定义此类键的顺序。
(在评论中,您提到 Scala 实际上定义了具有不变键类型的 Map[K, +V]。我从未使用过 Scala,但我敢打赌这就是原因。)