我阅读了维基百科文章存在类型。我了解到它们被称为存在类型,因为它们使用了存在运算符(∃)。但是我不确定它的意义是什么。那么,如何区分
T = ∃X { X a; int f(X); }
并且
T = ∀x { X a; int f(X); }
?
我阅读了维基百科文章存在类型。我了解到它们被称为存在类型,因为它们使用了存在运算符(∃)。但是我不确定它的意义是什么。那么,如何区分
T = ∃X { X a; int f(X); }
并且
T = ∀x { X a; int f(X); }
?
∀X
时,他们的意思是:你可以插入任何类型,我不需要知道关于类型的任何信息来完成我的工作,我只会将其不透明地称为X
。∃X
时,他们的意思是:我将在这里使用任何类型; 你不会知道有关类型的任何信息,因此你只能将其不透明地称为X
。void copy<T>(List<T> source, List<T> dest) {
...
}
copy
函数并不知道 T
到底是什么,但这并不影响它的使用。interface VirtualMachine<B> {
B compile(String source);
void run(B bytecode);
}
// Now, if you had a list of VMs you wanted to run on the same input:
void runAllCompilers(List<∃B:VirtualMachine<B>> vms, String source) {
for (∃B:VirtualMachine<B> vm : vms) {
B bytecode = vm.compile(source);
vm.run(bytecode);
}
}
runAllCompilers
并不知道字节码类型是什么,但它也不需要知道;它所做的就是将字节码从VirtualMachine.compile
转发到VirtualMachine.run
。List<?>
)是存在类型的一种非常有限的形式。// A wrapper that hides the type parameter 'B'
interface VMWrapper {
void unwrap(VMHandler handler);
}
// A callback (control inversion)
interface VMHandler {
<B> void handle(VirtualMachine<B> vm);
}
VMWrapper
调用我们自己的 VMHandler
,它具有通用类型的 handle
函数。总体效果是相同的,我们的代码必须将 B
视为不透明。void runWithAll(List<VMWrapper> vms, final String input)
{
for (VMWrapper vm : vms) {
vm.unwrap(new VMHandler() {
public <B> void handle(VirtualMachine<B> vm) {
B bytecode = vm.compile(input);
vm.run(bytecode);
}
});
}
}
一个虚拟机实现的例子:
class MyVM implements VirtualMachine<byte[]>, VMWrapper {
public byte[] compile(String input) {
return null; // TODO: somehow compile the input
}
public void run(byte[] bytecode) {
// TODO: Somehow evaluate 'bytecode'
}
public void unwrap(VMHandler handler) {
handler.handle(this);
}
}
∃B
明确量词的作用范围。使用通配符语法时,量词被隐含(实际上List<List<?>>
意味着∃T:List<List<T>>
而不是List<∃T:List<T>>
)。 此外,显式量化使您可以引用类型(我修改了示例以利用这一点,通过将类型B
的字节码存储在临时变量中来实现)。 - Kannan GoundanList<List<?>>
的意思是List<∃T:List<T>>
。在Java中没有办法指定∃T:List<List<T>>
。runAllCompilers
能够工作是因为在这种情况下限制恰好合适。 - Kannan Goundan∃x. F(x)
,包含某个类型 x
和一个类型为 F(x)
的值。而多态类型的值,如 ∀x. F(x)
,是一个函数,它接受某个类型 x
并生成一个类型为 F(x)
的值。在这两种情况下,类型都涵盖了一些类型构造器 F
。T = ∃X { X a; int f(X); }
T
的值包含了一个名为X
的类型,一个值a:X
和一个函数f:X->int
。类型为T
的值的生产者可以选择任何类型的X
,而消费者对X
一无所知,除了有一个叫做a
的例子,并且通过将其传递给f
,可以将此值转换为int
。换句话说,类型为T
的值知道如何以某种方式生成int
。我们可以消除中间类型X
,只需说:T = int
普遍量化的方式有所不同。
T = ∀X { X a; int f(X); }
T
的值可以赋予任何类型X
,它将产生一个值a:X
和一个函数f:X->int
,无论X
是什么。换句话说:类型为T
的值的使用者可以选择任何类型的X
。而类型为T
的值的生产者不能知道X
的任何信息,但必须能够为任何选择的X
生成一个值a
,并能够将这样的值转换为int
。
显然,实现这种类型是不可能的,因为没有程序能够产生每种想象得到的类型的值。除非你允许像null
或底部这样的荒谬情况。
由于存在类型是一对,因此通过柯里化,存在类型参数可以转换为通用类型参数。
(∃b. F(b)) -> Int
就相当于:
∀b. (F(b) -> Int)
前者是一个二阶存在量词。这导致了以下有用的属性:
每个存在量化的类型,排名为
n+1
,都是一个排名为n
的全称量化类型。
有一种将存在量化转换为全称量化的标准算法,称为Skolemization。
示例设置: 下面的伪代码并不是完全有效的Java,尽管很容易修复。事实上,这正是我将在本答案中做的!存在类型可以用于多种不同的目的。但它们的作用是在右侧“隐藏”类型变量。通常,任何出现在右侧的类型变量也必须出现在左侧[...]。
class Tree<α>
{
α value;
Tree<α> left;
Tree<α> right;
}
int height(Tree<α> t)
{
return (t != null) ? 1 + max( height(t.left), height(t.right) )
: 0;
}
<α> int height(Tree<α> t)
{
return (t != null) ? 1 + max( height(t.left), height(t.right) )
: 0;
}
这将允许您在该函数内声明变量并创建类型为α的表达式(如果您想要的话)。但是...
2. 存在类型解决方案:
如果您查看我们方法的主体,您会注意到我们实际上没有访问或处理任何类型为α的内容!没有任何具有该类型的表达式,也没有使用该类型声明的任何变量...那么,为什么我们需要使height
成为泛型呢?为什么我们不能简单地忘记α?事实证明,我们可以:
int height(Tree<?> t)
{
return (t != null) ? 1 + max( height(t.left), height(t.right) )
: 0;
}
height
更加通用,那么我们应该期望存在类型产生相反的影响:通过隐藏/删除类型参数α来使其不那么通用。t.value
的类型或操作任何该类型的表达式,因为没有标识符与之绑定。(?
通配符是一个特殊令牌,而不是“捕获”类型的标识符。)t.value
已经变得不透明了;也许你唯一还能做的就是将其类型转换为Object
。
总结:
===========================================================
| universally existentially
| quantified type quantified type
---------------------+-------------------------------------
calling method |
needs to know | yes no
the type argument |
---------------------+-------------------------------------
called method |
can use / refer to | yes no
the type argument |
=====================+=====================================
Object
进行比较非常有趣:虽然两者都类似于使您能够编写静态类型独立的代码,但前者(泛型)并不会为了实现这个目标而丢弃几乎所有可用的类型信息。在这个特定意义上,泛型在我看来是对Object
的一种补救。 - stakx - no longer contributingpublic static void swap(List<?> list, int i, int j) { swapHelper(list, i, j); } private static <E> void swapHelper(List<E> list, int i, int j) { list.set(i, list.set(j, list.get(i))); }
,E
是一个通用类型
,而?
代表的是一个存在类型
? - Kevin Meredithint height(Tree<?> t)
中的 ?
仍然未知,并且仍然由调用者确定,因为调用者可以选择传递哪棵树。即使人们称之为 Java 中的存在类型,它也不是。?
占位符可以在某些情况下用于实现 Java 中的存在类型,但这不是其中之一。 - Peter Hall这些都是好的例子,但我选择用稍微不同的方式来回答。从数学上回想一下,∀x.P(x)意味着“对于所有的x,我可以证明P(x)”。换句话说,它是一种函数,你给我一个x,我有一种方法来证明它。
在类型理论中,我们讨论的不是证明,而是类型。因此,在这个空间中,我们的意思是“对于你给我的任何类型X,我将给你一个特定的类型P”。现在,由于我们没有提供太多关于X的信息,除了它是一个类型,P对它的作用不大,但是有一些例子。P可以创建“相同类型的所有对”的类型:P<X> = Pair<X, X> = (X, X)
。或者我们可以创建选项类型:P<X> = Option<X> = X | Nil
,其中Nil是空指针的类型。我们可以把它变成列表:List<X> = (X, List<X>) | Nil
。请注意,最后一个是递归的,List<X>
的值既可以是第一个元素为X、第二个元素为List<X>
的对,也可以是空指针。
现在,在数学中,∃x.P(x)表示“我可以证明存在一个特定的x,使得P(x)为真”。可能有许多这样的x,但为了证明它,一个就足够了。另一种思考方法是,必须存在一个非空的证据和证明对集合{(x,P(x))}。
翻译为类型理论:家族中类型为 ∃X.P<X>
的类型包括类型 X 和相应的类型 P<X>
。需要注意的是,以前我们把 X 交给 P (这样我们对 X 了解得很多,但对 P 得知甚少),但现在情况正好相反。 P<X>
不保证提供任何有关 X 的信息,只是确定存在一个 X 类型。
这有什么用处呢?P 可能是一种具有公开其内部类型 X 的方法的类型。例如,一个对象隐藏了其状态 X 的内部表示。虽然我们不能直接操作它,但可以通过观察 P 的影响来了解其效果。可能会有许多实现这种类型的方式,但无论选择哪个特定的实现,都可以使用所有这些类型。
P<X>
而不是P
,这个函数会获得什么好处呢?(假设其功能和容器类型相同,但你不知道它是否包含X
) - ClaudiuP(x)
的断言。换句话说,它是一种函数,你给我一个x,我有一种方法来为你证明它。-- 这些是无意义的话语。 - Jim Balter直接回答您的问题:
使用通用类型时,T
的用法必须包括类型参数X
。例如:T<String>
或者T<Integer>
。而对于存在类型的用法,T
不包含这个类型参数因为它未知或无关紧要 - 只需使用T
(或在Java中使用T<?>
)。
更多信息:
通用/抽象类型和存在类型是对象/函数的使用者/客户端和生产者/实现者之间透视角度的二元性。当一方看到通用类型时,另一方看到存在类型。
在Java中,您可以定义一个泛型类:
public class MyClass<T> {
// T is existential in here
T whatever;
public MyClass(T w) { this.whatever = w; }
public static MyClass<?> secretMessage() { return new MyClass("bazzlebleeb"); }
}
// T is universal from out here
MyClass<String> mc1 = new MyClass("foo");
MyClass<Integer> mc2 = new MyClass(123);
MyClass<?> mc3 = MyClass.secretMessage();
MyClass
的客户端视角来看,T
是通用的,因为你可以在使用该类时替换任何类型的T
,但你必须知道每次使用MyClass
实例时T
的实际类型。MyClass
内部实例方法的视角来看,T
是存在的,因为它并不知道T
的真实类型。?
代表存在类型 - 因此当你在类内部时,T
基本上就是?
。如果你想处理具有存在类型T
的MyClass
实例,你可以像上面的secretMessage()
示例那样声明MyClass<?>
。存在类型有时被用于隐藏某些实现细节,这个话题已经在其他地方讨论过了。一个Java版本的例子可能是:
public class ToDraw<T> {
T obj;
Function<Pair<T,Graphics>, Void> draw;
ToDraw(T obj, Function<Pair<T,Graphics>, Void>
static void draw(ToDraw<?> d, Graphics g) { d.draw.apply(new Pair(d.obj, g)); }
}
// Now you can put these in a list and draw them like so:
List<ToDraw<?>> drawList = ... ;
for(td in drawList) ToDraw.draw(td);
看起来我来晚了,但不管怎样,这篇文档提供了另一种关于存在类型的视角,虽然不是特别与语言无关,但应该相对容易理解存在类型:Dijkstra, Atze - Essential Haskell(第8章)。
普遍量化类型和存在量化类型之间的区别可以通过以下观察来描述:let exfile = fopen("foo.txt"); // No type for exfile!
read(exfile, buf, size);
接口“read”的声明如下:
存在类型T,使得:
size_t read(T exfile, char* buf, size_t size);
变量exfile既不是int类型,也不是char*
类型,也不是struct File类型——在类型系统中无法表达。你不能声明一个类型未知的变量,也不能将指针转换为该未知类型。语言不允许这样做。
read
的签名是∃T.read(T file, ...)
,那么你无法将任何东西作为第一个参数传递。可行的方法是让fopen
返回文件句柄和一个由相同存在范围限定的读取函数:∃T.(T, read(T file, ...))
。 - Kannan Goundanread
?你如何将其与其他“无类型”的变量区分开来?“在类型系统中无法表达的任何内容” - 那么你的语言设计是有问题的。 - Jim Baltertrait Existential {
type Parameter <: Interface
}
trait Existential[Parameter <: Interface]
List[_]
在Scala中和Java中的List<?>
的速记符号。∀x.f(x)
对其接收函数是不透明的,而存在类型 ∃x.f(x)
则被限制为具有某些属性。通常,所有参数都是存在类型,因为它们的函数将直接操作它们;但是,通用参数可能具有通用类型,因为该函数不会在基本通用操作之外管理它们,例如获取引用,如:∀x.∃array.copy(src:array[x] dst:array[x]){...}
。 - George