Что такое экзистенциальный тип? - PullRequest
157 голосов
/ 15 ноября 2008

Я прочитал статью в Википедии Экзистенциальные типы . Я понял, что они называются экзистенциальными типами из-за экзистенциального оператора (∃). Я не уверен, какой в ​​этом смысл. В чем разница между

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

и

T = ∀x { X a; int f(X); }

Ответы [ 11 ]

170 голосов
/ 02 апреля 2011

Когда кто-то определяет универсальный тип ∀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<?>) - это очень ограниченная форма экзистенциальных типов.

Обновление: Забыл упомянуть, что вы можете сортировать экзистенциальные типы с помощью универсальных типов. Сначала оберните ваш универсальный тип, чтобы скрыть параметр типа. Во-вторых, инвертированный контроль (это эффективно меняет часть «ты» и «я» в вышеприведенных определениях, что является основным отличием экзистенциалов и универсалий).

// 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);
   }
}
96 голосов
/ 28 февраля 2012

Значение экзистенциального типа , подобного ∃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, и что это значение можно превратить в 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 или низы.

Поскольку экзистенциальный является парой, экзистенциальный аргумент может быть преобразован в универсальный с помощью curry .

(∃b. F(b)) -> Int

совпадает с:

∀b. (F(b) -> Int)

Первый является экзистенциальным rank-2 . Это приводит к следующему полезному свойству:

Каждый экзистенциально количественный тип ранга n+1 является универсально квантифицированным типом ранга n.

Существует стандартный алгоритм превращения экзистенциалов в универсалии, который называется Сколемизация .

30 голосов
/ 31 декабря 2011

Я думаю, что имеет смысл объяснять экзистенциальные типы вместе с универсальными типами, поскольку эти два понятия дополняют друг друга, то есть одно является «противоположностью» другого.

Я не могу ответить на каждую деталь об экзистенциальных типах (например, дать точное определение, перечислить все возможные применения, их отношение к абстрактным типам данных и т. Д.), Потому что я просто недостаточно осведомлен для этого. Я продемонстрирую только (с использованием 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;
}

Позвольте мне кратко изложить это для вас. Мы определяем & hellip;

  • рекурсивный тип 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   |                  
=====================+=====================================
13 голосов
/ 08 сентября 2012

Это все хорошие примеры, но я решил ответить на это немного по-другому. Напомним из математики, что ∀x. P (x) означает «для всех x, я могу доказать, что P (x)». Другими словами, это своего рода функция, вы даете мне крестик, и у меня есть метод, чтобы доказать это для вас.

В теории типов мы говорим не о доказательствах, а о типах. Таким образом, в этом пространстве мы подразумеваем «для любого типа X, который вы мне дадите, я дам вам конкретный тип P». Теперь, поскольку мы не даем 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>, либо это нулевой указатель.

Теперь по математике. P (x) означает «Я могу доказать, что существует конкретный x такой, что P (x) верен». Таких иксов может быть много, но для доказательства достаточно одного. Другой способ думать об этом состоит в том, что должен существовать непустой набор пар доказательство-доказательство {(x, P (x))}.

Переведено в теорию типов: тип в семействе ∃X.P<X> - это тип X и соответствующий тип P<X>. Обратите внимание на то, что до того, как мы дали X X P (так что мы знали о X все, но P очень мало), теперь все наоборот. P<X> не обещает предоставлять какую-либо информацию о X, только то, что там есть один, и что это действительно тип.

Чем это полезно? Ну, P может быть типом, который имеет способ раскрытия своего внутреннего типа X. Примером может быть объект, который скрывает внутреннее представление своего состояния X. Хотя у нас нет никакого способа непосредственно манипулировать им, мы можем наблюдать его эффект, тыкаешь в P. Может быть много реализаций этого типа, но вы можете использовать все эти типы независимо от того, какой именно был выбран.

12 голосов
/ 13 апреля 2009

Экзистенциальный тип является непрозрачным типом.

Подумайте о дескрипторе файла в 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, не char*, не struct file - ничего, что вы можете выразить в системе типов. Вы не можете объявить переменную, тип которой неизвестен, и вы не можете привести, скажем, указатель к этому неизвестному типу. Язык не позволит вам.

10 голосов
/ 30 ноября 2012

Чтобы прямо ответить на ваш вопрос:

В универсальном типе использование 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 при использовании этого класса, и вы должны знать фактический тип T всякий раз, когда вы используете экземпляр MyClass
  • С точки зрения методов экземпляра в самом MyClass, T является экзистенциальным, поскольку не знает реального типа T
  • В Java ? представляет экзистенциальный тип - таким образом, когда вы находитесь внутри класса, T в основном ?. Если вы хотите обработать экземпляр MyClass с помощью T existential, вы можете объявить 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, позволяющий помещать в список множество различных типов данных и преобразовывать их все в целое число (или выполнять любые операции, предусмотренные внутри структуры данных) по требованию. .

Практически всегда можно преобразовать запись с экзистенциальным типом в запись без использования замыканий. Закрытие также типизировано экзистенциально, так как свободные переменные, по которым оно закрыто, скрыты от вызывающей стороны. Таким образом, язык, который поддерживает замыкания, но не экзистенциальные типы, может позволить вам создавать замыкания, которые имеют то же скрытое состояние, которое вы бы поместили в экзистенциальную часть объекта.

6 голосов
/ 04 ноября 2015

Кажется, я немного опаздываю, но в любом случае, этот документ добавляет другое представление о том, что такое экзистенциальные типы, хотя они не являются специфически независимыми от языка, тогда должно быть довольно легко понять экзистенциальные типы: http://www.cs.uu.nl/groups/ST/Projects/ehc/ehc-book.pdf (глава 8)

Разница между универсально и экзистенциально определенным типом может быть охарактеризована следующим наблюдением:

  • Использование значения с квантифицированным типом определяет тип, который нужно выбрать для создания экземпляра квантифицированной переменной типа. Например, вызывающая функция идентификатора «id :: .a.a → a» определяет тип, который нужно выбрать для переменной типа a для этого конкретного применения id. Для приложения функции «id 3» этот тип равен Int.

  • Создание значения с квантифицированным типом определяет и скрывает тип квантифицированной переменной типа. Например, создатель «.a. (A, a → Int)» мог построить значение этого типа из «(3, λx → x)»; другой создатель создал значение с таким же типом из «(’ x ’, λx → ord x)». С точки зрения пользователя оба значения имеют одинаковый тип и, таким образом, являются взаимозаменяемыми. У значения есть определенный тип, выбранный для переменной типа a, но мы не знаем, какой тип, поэтому эта информация больше не может быть использована. Эта специфичная для значения информация о типе была «забыта»; мы только знаем, что он существует.

4 голосов
/ 15 июня 2015

Универсальный тип существует для всех значений параметра (ов) типа. Экзистенциальный тип существует только для значений параметра (ов) типа, которые удовлетворяют ограничениям экзистенциального типа.

Например, в Scala одним из способов выражения экзистенциального типа является абстрактный тип, который ограничен некоторыми верхними или нижними границами.

trait Existential {
  type Parameter <: Interface
}

Эквивалентно ограниченный универсальный тип - это экзистенциальный тип, как в следующем примере.

trait Existential[Parameter <: Interface]

Любой используемый сайт может использовать Interface, потому что любые инстанцируемые подтипы Existential должны определять type Parameter, который должен реализовывать Interface.

A вырожденный регистр экзистенциального типа в Scala - это абстрактный тип, на который никогда не ссылаются, и, следовательно, его не нужно определять ни для какого подтипа. Это эффективно имеет сокращенную запись List[_] в Scala и List<?> в Java.

Мой ответ был вдохновлен предложением Мартина Одерского объединить абстрактные и экзистенциальные типы. сопровождающий слайд помогает понять.

3 голосов
/ 24 ноября 2008

Исследования абстрактных типов данных и сокрытия информации привели экзистенциальные типы в языки программирования. Создание абстрактного типа данных скрывает информацию об этом типе, поэтому клиент этого типа не может злоупотреблять им. Скажем, у вас есть ссылка на объект ... некоторые языки позволяют вам преобразовывать эту ссылку в ссылку на байты и делать все, что вы хотите, с этим фрагментом памяти. В целях гарантии поведения программы для языка полезно обеспечить, чтобы вы действовали только в отношении ссылки на объект с помощью методов, предоставляемых конструктором объекта. Вы знаете, что тип существует, но не более того.

См:

Абстрактные типы имеют экзистенциальный тип, MITCHEL & PLOTKIN

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

1 голос
/ 05 декабря 2018

Я создал эту диаграмму. Я не знаю, строго ли это. Но если это поможет, я рад. enter image description here

...