Answers:
当某人定义通用类型时,∀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
。
Java类型通配符(例如:)List<?>
是存在类型的一种非常有限的形式。
更新:忘记提及您可以使用通用类型来模拟存在类型。首先,包装通用类型以隐藏type参数。其次,反转控制(这有效地交换了以上定义中的“ you”和“ I”部分,这是存在性和通用性之间的主要区别)。
// 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);
}
});
}
}
VM实现示例:
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);
}
}
List<∃B:VirtualMachine<B>> vms
或中数学符号的含义for (∃B:VirtualMachine<B> vm : vms)
。(由于这些是泛型类型,您不能使用Java的?
通配符代替“自制”语法吗?)我认为,有一个代码示例中不∃B:VirtualMachine<B>
涉及诸如此类的泛型类型,而应使用“直的” 类型可能会有所帮助∃B
,因为在您的第一个代码示例之后,泛型类型很容易与通用类型相关联。
∃B
以前很明确地说明了量化发生的地方。使用通配符语法时,会隐含量词(List<List<?>>
实际上是意思∃T:List<List<T>>
,不是List<∃T:List<T>>
)。另外,显式量化使您可以引用类型(我将示例类型修改为通过将类型的字节码存储B
在临时变量中来利用此类型)。
存在类型的值,例如,∃x. F(x)
是包含某种类型 x
和该类型的值的一对F(x)
。而类似polymorphic类型的值∀x. F(x)
是一个具有某种类型并产生 type值的函数。在这两种情况下,类型都会关闭某个类型构造函数。x
F(x)
F
请注意,此视图混合了类型和值。存在证明是一种类型和一种价值。通用证明是按类型(或从类型到值的映射)索引的整个值系列。
因此,您指定的两种类型之间的区别如下:
T = ∃X { X a; int f(X); }
这意味着:类型的值T
包含一个称为的类型X
,一个值a:X
和一个函数f:X->int
。类型值的生产者T
可以选择任何类型,X
消费者对此一无所知X
。除了调用它的一个示例,a
并且可以通过将其int
赋予将该值转换为f
。换句话说,类型的值T
知道如何产生int
某种方式。好吧,我们可以消除中间类型X
而只是说:
T = int
普遍量化的数字有些不同。
T = ∀X { X a; int f(X); }
这意味着:类型的值T
可以给出任何类型的X
,它会产生一个值a:X
,和功能f:X->int
不管是什么X
是。换句话说:类型值的使用者T
可以为选择任何类型X
。类型值的产生者T
一无所知X
,但是它必须能够a
为任何选择产生一个值X
,并能够将这样的值转化为一个值int
。
显然,实现这种类型是不可能的,因为没有程序可以产生每种可以想象的类型的值。除非您允许这样的荒谬null
或沉迷。
由于一个存在性参数是一对,因此可以通过currying将一个存在性参数转换为通用参数。
(∃b. F(b)) -> Int
是相同的:
∀b. (F(b) -> Int)
前者是2级生存者。这导致以下有用的属性:
等级的每个存在量化类型都是等级
n+1
的普遍量化类型n
。
有一种标准的算法可以将存在物转换为通用物,称为Skolemization。
我认为将存在性类型与通用类型一起解释是有意义的,因为这两个概念是互补的,即一个概念与另一个概念“相对”。
我无法回答有关存在性类型的每一个细节(例如给出确切的定义,列出所有可能的用途,它们与抽象数据类型的关系等),因为我对此没有足够的了解。我将仅演示(使用Java)HaskellWiki文章声明存在类型的主要作用:
存在类型可以用于多种不同目的。但是他们要做的是在右侧“隐藏”一个类型变量。通常,任何出现在右侧的类型变量也必须出现在左侧[…]
设置示例:
以下伪代码不是完全有效的Java,即使很容易修复它也是如此。实际上,这正是我在此答案中要做的!
class Tree<α>
{
α value;
Tree<α> left;
Tree<α> right;
}
int height(Tree<α> t)
{
return (t != null) ? 1 + max( height(t.left), height(t.right) )
: 0;
}
让我为您简要说明一下。我们正在定义...
Tree<α>
表示二进制树中节点的递归类型。每个节点存储value
某种类型的α,并引用相同类型的可选树left
和right
子树。
该函数height
返回从任何叶节点到根节点的最远距离t
。
现在,让我们将上面的伪代码height
转换为正确的Java语法!(为简洁起见,我将继续省略一些样板,例如面向对象和可访问性修饰符。)我将展示两种可能的解决方案。
1.通用型解决方案:
最明显的解决方法是height
通过将类型参数α引入其签名来简单地实现泛型:
<α> 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
IMO的一种补救。
public 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))); }
,@ stakx E
是a universal type
并?
表示a existential type
?
?
in类型int height(Tree<?> t)
仍然是未知的,并且仍然由调用者确定,因为调用者必须选择要传入的树。即使人们在Java中将其称为存在类型,也并非如此。该?
占位符可以被用来实现在Java中existentials,在某些情况下的一种形式,但是这是不是其中之一。
这些都是很好的例子,但是我选择回答的方式有所不同。回忆一下数学。P(x)表示“对于所有x,我都可以证明P(x)”。换句话说,它是一种函数,您给我一个x,我有一种方法可以为您证明。
在类型论中,我们不是在谈论证明,而是类型。因此,在这个空间中,我们的意思是“对于您给我的任何类型X,我都会给您提供特定的类型P”。现在,由于除了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,第二个元素是a 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>
而不是a P
(假设功能和容器类型相同,但是您不知道它包含X
),该函数会获得什么呢?
∀x. P(x)
关于的可证性并没有任何意义P(x)
,仅是事实。
要直接回答您的问题:
对于通用类型,的使用T
必须包括type参数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
,当你使用这个类,你必须知道实际的类型T的,只要你使用的实例MyClass
MyClass
本身的角度来看,T
是存在的,因为它不知道T
?
表示存在类型-因此,当您在类中时,T
基本上是?
。如果您要处理MyClass
具有T
存在性的实例,则可以MyClass<?>
按照上述secretMessage()
示例进行声明。如其他地方所讨论的,存在类型有时用于隐藏某些东西的实现细节。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);
正确地捕获它有点棘手,因为我假装使用某种功能性编程语言,而Java不是。但是这里的要点是,您正在捕获某种状态以及在该状态上运行的函数的列表,并且您不知道状态部分的真实类型,但是这些函数可以捕获,因为它们已经与该类型匹配。
现在,在Java中,所有非最终非原始类型都是部分存在的。这听起来有些奇怪,但是因为声明为的变量Object
可能是的子类。Object
,所以您不能声明特定类型,只能声明“此类型或子类”。因此,对象被表示为状态的一部分以及在该状态下运行的函数的列表-确切地说,要调用的函数是在运行时通过查找确定的。这非常类似于上面的存在类型的使用,在该类型中,您具有一个存在状态部分和在该状态上运行的函数。
在没有子类型和类型转换的静态类型编程语言中,存在类型允许人们管理不同类型对象的列表。的列表T<Int>
不能包含T<Long>
。但是,的列表T<?>
可以包含的任何变体T
,从而允许按需将许多不同类型的数据放入列表并将其全部转换为int(或执行数据结构内部提供的任何操作)。
几乎可以始终将具有存在性类型的记录转换为记录,而无需使用闭包。闭包也是现有类型的,因为闭包的自由变量对调用者隐藏。因此,一种支持闭包但不存在类型的语言可以使您创建与对象的存在部分共享相同隐藏状态的闭包。
存在类型是不透明类型。
考虑一下Unix中的文件句柄。您知道它的类型是int,所以您可以轻松地伪造它。例如,您可以尝试从句柄43中读取。如果碰巧该程序使用此特定的句柄打开了文件,则可以从中读取。您的代码不一定是恶意的,而是草率的(例如,句柄可能是未初始化的变量)。
程序中隐藏了一个存在性类型。如果fopen
返回一个存在性类型,您所能做的就是将其与接受该存在性类型的某些库函数一起使用。例如,以下伪代码将编译:
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,不是a char*
,不是struct File-您在类型系统中无法表达任何内容。您不能声明类型未知的变量,也不能将指针转换为该未知类型。语言不会让你。
read
为∃T.read(T file, ...)
则没有任何内容可以作为第一个参数传递。可行的方法是fopen
返回文件句柄和一个由相同的存在变量限定范围的读取函数:∃T.(T, read(T file, ...))
看来我来晚了一点,但是无论如何,该文档添加了关于存在性类型的另一种观点,尽管不是专门与语言无关的,但理解存在性类型应该相当容易:http://www.cs.uu .nl / groups / ST / Projects / ehc / ehc-book.pdf(第8章)
普遍量化类型和生存量化类型之间的差异可以通过以下观察来表征:
量化类型为∀的值的使用确定了量化类型变量的实例化要选择的类型。例如,身份函数“ id ::∀aa→a”的调用者确定为id的此特定应用程序的类型变量a选择的类型。对于功能应用程序“ id 3”,此类型等于Int。
量化类型为∃的值的创建将确定并隐藏量化类型变量的类型。例如,“∃a。(a,a→Int)”的创建者可能已经从“(3,λx→x)”构造了该类型的值;另一个创建者使用“('x',λx→ord x)”构造了一个具有相同类型的值。从用户的角度来看,两个值具有相同的类型,因此可以互换。该值具有为类型变量a选择的特定类型,但是我们不知道是哪种类型,因此无法再利用此信息。该值特定类型信息已被“遗忘”;我们只知道它的存在。
类型参数的所有值都存在一个通用类型。仅对于满足存在类型约束的类型参数值,存在存在类型。
例如,在Scala中,表示存在类型的一种方法是将抽象类型限制在某些上限或下限。
trait Existential {
type Parameter <: Interface
}
等效地,约束通用类型是存在类型,如以下示例所示。
trait Existential[Parameter <: Interface]
任何使用站点都可以使用,Interface
因为的任何可实例化子类型都Existential
必须定义type Parameter
必须实现的。Interface
。
甲退化情况 Scala中一种存在类型的是一种抽象的类,其从未提及,因此不需要由任何亚型来定义。这实际上List[_]
在Scala和List<?>
Java。
∀x.f(x)
对于它们的接收函数是不透明的,而∃x.f(x)
现有类型,则被限制为具有某些属性。通常,所有参数都是存在的,因为它们的功能将直接操纵它们。但是,通用参数的类型可能是通用的,因为该函数将无法在基本通用操作(例如,获取引用)之外进行管理,例如:∀x.∃array.copy(src:array[x] dst:array[x]){...}
forSome
类型参数存在量化。
对抽象数据类型和信息隐藏的研究将存在性类型带入了编程语言。将数据类型做成抽象会隐藏有关该类型的信息,因此该类型的客户端不能滥用它。假设您有对一个对象的引用...某些语言允许您将该引用转换为对字节的引用,并对该内存进行任何处理。为了保证程序的行为,一种语言强制您仅通过对象设计者提供的方法对对象的引用进行操作很有用。您知道类型存在,但仅此而已。
看到:
抽象类型具有存在类型,MITCHEL和PLOTKIN