什么是存在类型?


Answers:


192

当某人定义通用类型时,∀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.compileVirtualMachine.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);
   }
}

12
@ Kannan,+ 1是一个非常有用但有点难以把握的答案:1.我认为,如果您可以更明确地了解存在和通用类型的双重性质,这将有所帮助。我只是偶然地意识到,您对前两段的表述非常相似。直到稍后您才明确声明这两个定义基本相同,但“ I”和“ you”相反。另外,我并没有立即理解“我”和“你”应该指的是什么。
stakx-在11:30不再贡献

2
(续:)2.我不完全了解List<∃B:VirtualMachine<B>> vms或中数学符号的含义for (∃B:VirtualMachine<B> vm : vms)。(由于这些是泛型类型,您不能使用Java的?通配符代替“自制”语法吗?)我认为,有一个代码示例中不∃B:VirtualMachine<B>涉及诸如此类的泛型类型,而应使用“直的” 类型可能会有所帮助∃B,因为在您的第一个代码示例之后,泛型类型很容易与通用类型相关联。
stakx-不再贡献

2
∃B以前很明确地说明了量化发生的地方。使用通配符语法时,会隐含量词(List<List<?>>实际上是意思∃T:List<List<T>>,不是List<∃T:List<T>>)。另外,显式量化使您可以引用类型(我将示例类型修改为通过将类型的字节码存储B在临时变量中来利用此类型)。
Kannan Goundan'1

2
这里使用的数学符号像地狱一样草率,但是我不认为这是应答者的错(这是标准的)。尽管如此,最好还是不要以这种方式滥用存在和通用量词……
Noldorin 2012年

2
@Kannan_Goundan,我想知道是什么让您说Java通配符是此版本的非常有限的版本。您是否知道可以在纯Java中实现您的第一个runAllCompilers示例函数(使用辅助函数来检索(为通配符提供参数)名称)?
LP_

107

存在类型的值,例如,∃x. F(x) 包含某种类型 x和该类型的的一对F(x)。而类似polymorphic类型的值∀x. F(x)是一个具有某种类型并产生 type值的函数。在这两种情况下,类型都会关闭某个类型构造函数。xF(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



34

我认为将存在性类型与通用类型一起解释是有意义的,因为这两个概念是互补的,即一个概念与另一个概念“相对”。

我无法回答有关存在性类型的每一个细节(例如给出确切的定义,列出所有可能的用途,它们与抽象数据类型的关系等),因为我对此没有足够的了解。我将仅演示(使用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某种类型的α,并引用相同类型的可选树leftright子树。

  • 该函数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   |                  
=====================+=====================================

3
很好的解释。您无需将t.value强制转换为Object,只需将其称为Object。因此,存在类型将使方法更通用。关于t.value,您唯一了解的就是它是一个对象,而您可能已经说过有关α的一些特定信息(例如α扩展了Serializable)。
Craig P. Motlin

1
同时,我已经相信我的答案并不能真正解释什么是存在论,我正在考虑写另一本书,更像是坎南·古丹的答案的前两段,我认为这更接近“真相”。话虽如此,@ Craig:将泛型与Object进行比较非常有趣:虽然两者相似,但它们使您能够编写静态类型独立的代码,但是前者(泛型)并不会将几乎所有可用的类型信息扔掉。达成这个目标。从这个特殊的意义上讲,泛型是对ObjectIMO的一种补救。
stakx-不再贡献2012年

1
在此代码中(来自Effective Java)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,在某些情况下的一种形式,但是这是不是其中之一。
彼得·霍尔

15

这些都是很好的例子,但是我选择回答的方式有所不同。回忆一下数学。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。可能有许多这种类型的实现,但是无论选择哪种特定类型,都可以使用所有这些类型。


2
嗯,但是通过知道它是a P<X>而不是a P(假设功能和容器类型相同,但是您不知道它包含X),该函数会获得什么呢?
Claudiu 2012年

3
严格来说,∀x. P(x)关于的可证性并没有任何意义P(x),仅是事实。
R .. GitHub停止帮助ICE

11

要直接回答您的问题:

对于通用类型,的使用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();
  • 从的角度来看客户端MyClassT是普遍的,因为你可以代替任何类型T,当你使用这个类,你必须知道实际的类型T的,只要你使用的实例MyClass
  • 从实例方法MyClass本身的角度来看,T是存在的,因为它不知道T
  • 在Java中,?表示存在类型-因此,当您在类中时,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(或执行数据结构内部提供的任何操作)。

几乎可以始终将具有存在性类型的记录转换为记录,而无需使用闭包。闭包也是现有类型的,因为闭包的自由变量对调用者隐藏。因此,一种支持闭包但不存在类型的语言可以使您创建与对象的存在部分共享相同隐藏状态的闭包。


11

存在类型是不透明类型。

考虑一下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-您在类型系统中无法表达任何内容。您不能声明类型未知的变量,也不能将指针转换为该未知类型。语言不会让你。


9
这行不通。如果签名read∃T.read(T file, ...)则没有任何内容可以作为第一个参数传递。可行的方法是fopen返回文件句柄一个由相同的存在变量限定范围的读取函数:∃T.(T, read(T file, ...))
Kannan Goundan 2011年

2
这似乎只是在谈论ADT。
kizzx2 2011年

7

看来我来晚了一点,但是无论如何,该文档添加了关于存在性类型的另一种观点,尽管不是专门与语言无关的,但理解存在性类型应该相当容易: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选择的特定类型,但是我们不知道是哪种类型,因此无法再利用此信息。该值特定类型信息已被“遗忘”;我们只知道它的存在。


1
尽管此链接可以回答问题,但最好在此处包括答案的基本部分,并提供链接以供参考。如果链接的页面发生更改,仅链接的答案可能会失效。
sheilak 2015年

1
@sheilak:更新了答案,感谢您的建议
themarketka

5

类型参数的所有值都存在一个通用类型。仅对于满足存在类型约束的类型参数值,存在存在类型。

例如,在Scala中,表示存在类型的一种方法是将抽象类型限制在某些上限或下限。

trait Existential {
  type Parameter <: Interface
}

等效地,约束通用类型是存在类型,如以下示例所示。

trait Existential[Parameter <: Interface]

任何使用站点都可以使用,Interface因为的任何可实例化子类型都Existential必须定义type Parameter必须实现的。Interface

退化情况 Scala中一种存在类型的是一种抽象的类,其从未提及,因此不需要由任何亚型来定义。这实际上List[_] 在ScalaList<?> Java。

我的回答受到Martin Odersky 关于统一抽象和存在类型的建议的启发。在伴随滑动有助于理解。


1
仔细阅读以上内容后,您似乎已经很好地总结了我的理解:通用类型,∀x.f(x)对于它们的接收函数是不透明的,而∃x.f(x)现有类型,则被限制为具有某些属性。通常,所有参数都是存在的,因为它们的功能将直接操纵它们。但是,通用参数的类型可能是通用的,因为该函数将无法在基本通用操作(例如,获取引用)之外进行管理,例如:∀x.∃array.copy(src:array[x] dst:array[x]){...}
乔治

如此处所述stackoverflow.com/a/19413755/3195266类型成员可以通过标识类型来模拟通用量化。并且肯定有forSome类型参数存在量化。
Netsu

3

对抽象数据类型和信息隐藏的研究将存在性类型带入了编程语言。将数据类型做成抽象会隐藏有关该类型的信息,因此该类型的客户端不能滥用它。假设您有对一个对象的引用...某些语言允许您将该引用转换为对字节的引用,并对该内存进行任何处理。为了保证程序的行为,一种语言强制您仅通过对象设计者提供的方法对对象的引用进行操作很有用。您知道类型存在,但仅此而已。

看到:

抽象类型具有存在类型,MITCHEL和PLOTKIN

http://theory.stanford.edu/~jcm/papers/mitch-plotkin-88.pdf


1

我创建了这个图。我不知道它是否严格。但是,如果有帮助,我很高兴。 在此处输入图片说明


-6

据我了解,这是描述接口/抽象类的数学方法。

至于T =∃X{X a; int f(X); }

对于C#,它将转换为通用抽象类型:

abstract class MyType<T>{
    private T a;

    public abstract int f(T x);
}

“存在”仅表示存在某种类型的内容,可以遵循此处定义的规则。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.