Przeczytałem artykuł w Wikipedii Typy egzystencjalne . Dowiedziałem się, że nazywa się je typami egzystencjalnymi ze względu na operator egzystencjalny (∃). Nie jestem jednak pewien, jaki to ma sens. Jaka jest różnica pomiędzy
T = ∃X { X a; int f(X); }
i
T = ∀x { X a; int f(X); }
?
Odpowiedzi:
Kiedy ktoś definiuje typ uniwersalny
∀X
, mówi: Możesz podłączyć dowolny typ, nie muszę nic wiedzieć o typie, aby wykonać swoją pracę, będę się do niego odnosił nieprzejrzysto jakoX
.Kiedy ktoś definiuje typ egzystencjalny
∃X
, mówi: Użyję tutaj dowolnego typu; nie będziesz wiedział nic o typie, więc możesz nazywać go nieprzejrzystym językiemX
.Uniwersalne typy pozwalają pisać takie rzeczy jak:
copy
Funkcja nie ma pojęcia, coT
rzeczywiście będzie, ale to nie jest konieczne.Typy egzystencjalne pozwolą Ci pisać takie rzeczy, jak:
Każda implementacja maszyny wirtualnej na liście może mieć inny typ kodu bajtowego.
runAllCompilers
Funkcja nie ma pojęcia, co rodzaj kodu bajtowego jest, ale to nie jest konieczne; wszystko, co robi, to przekazywanie kodu bajtowego zVirtualMachine.compile
doVirtualMachine.run
.Symbole wieloznaczne typu Java (np.:)
List<?>
To bardzo ograniczona forma typów egzystencjalnych.Aktualizacja: zapomniałem wspomnieć, że możesz symulować typy egzystencjalne za pomocą typów uniwersalnych. Najpierw zawiń swój typ uniwersalny, aby ukryć parametr typu. Po drugie, odwrócenie kontroli (to skutecznie zamienia część „ty” i „ja” w powyższych definicjach, co jest podstawową różnicą między egzystencjalnymi i uniwersalnymi).
Teraz możemy mieć
VMWrapper
własne wywołanie,VMHandler
które ma uniwersalnąhandle
funkcję. Efekt netto jest taki sam, nasz kod musi być traktowanyB
jako nieprzejrzysty.Przykładowa implementacja maszyny wirtualnej:
źródło
List<∃B:VirtualMachine<B>> vms
lubfor (∃B:VirtualMachine<B> vm : vms)
. (Ponieważ są to typy ogólne, czy nie mógłbyś użyć?
symboli wieloznacznych Javy zamiast składni „samodzielnie utworzonej”?) Myślę, że przydałby się przykład kodu, w którym nie są używane żadne typy ogólne∃B:VirtualMachine<B>
, ale zamiast tego „prosta”∃B
, ponieważ typy ogólne można łatwo powiązać z typami uniwersalnymi po pierwszych przykładach kodu.∃B
wprost o tym, gdzie odbywa się kwantyfikacja. W składni symboli wieloznacznych implikowany jest kwantyfikator (wList<List<?>>
rzeczywistości oznacza,∃T:List<List<T>>
a nieList<∃T:List<T>>
). Również jawna kwantyfikacja pozwala na odwołanie się do typu (zmodyfikowałem przykład, aby to wykorzystać, przechowując kod bajtowy typuB
w zmiennej tymczasowej).Wartość typu egzystencjalnego, takiego jak,
∃x. F(x)
jest parą zawierającą pewien typx
i wartość typuF(x)
. Natomiast wartość typu polimorficznego, takiego jak,∀x. F(x)
jest funkcją, która przyjmuje pewien typx
i wytwarza wartość typuF(x)
. W obu przypadkach typ zamyka się na jakimś konstruktorze typówF
.Zwróć uwagę, że ten widok łączy typy i wartości. Dowód egzystencjalny to jeden typ i jedna wartość. Dowód uniwersalny to cała rodzina wartości indeksowanych według typu (lub odwzorowanie typów na wartości).
Zatem różnica między dwoma określonymi typami jest następująca:
Oznacza to: Wartość typu
T
zawiera wywołany typX
, wartośća:X
i funkcjęf:X->int
. Producent wartości typuT
może wybrać dowolny typ,X
a konsument nie może nic wiedziećX
. Tyle tylko, że istnieje jeden przykład o nazwiea
i że tę wartość można zamienić na aint
, podając jąf
. Innymi słowy, wartość typuT
wie, jak wint
jakiś sposób utworzyć . Cóż, możemy wyeliminować typ pośredniX
i po prostu powiedzieć:Ten, który jest określany ilościowo, jest trochę inny.
Oznacza to, że: Wartość typu
T
może mieć dowolny typX
i wygeneruje wartośća:X
i funkcjęf:X->int
bez względu na to, co niąX
jest . Innymi słowy: konsument wartości typuT
może wybrać dowolny typX
. A wytwórca wartości typuT
nie może w ogóle nic wiedziećX
, ale musi być w stanie wytworzyć wartośća
dla dowolnego wyboruX
i być w stanie przekształcić taką wartość wint
.Oczywiście implementacja tego typu jest niemożliwa, ponieważ nie ma programu, który mógłby wygenerować wartość każdego możliwego do wyobrażenia typu. Chyba że pozwolisz na absurdy takie jak
null
lub dno.Ponieważ egzystencjalny jest parą, egzystencjalny argument można przekształcić w uniwersalny za pomocą curry .
jest taki sam jak:
Ten pierwszy jest egzystencjalny na drugim miejscu . Prowadzi to do następującej użytecznej właściwości:
Istnieje standardowy algorytm przekształcania egzystencji w uniwersalia, zwany skolemizacją .
źródło
Myślę, że sensowne jest wyjaśnianie typów egzystencjalnych razem z typami uniwersalnymi, ponieważ te dwa pojęcia są komplementarne, tj. Jedno jest „przeciwieństwem” drugiego.
Nie mogę odpowiedzieć na wszystkie szczegóły dotyczące typów egzystencjalnych (takich jak podanie dokładnej definicji, lista wszystkich możliwych zastosowań, ich związek z abstrakcyjnymi typami danych itp.), Ponieważ po prostu nie mam do tego wystarczającej wiedzy. Pokażę tylko (używając Java), co ten artykuł HaskellWiki określa jako główny efekt typów egzystencjalnych:
Przykładowa konfiguracja:
Poniższy pseudo-kod nie jest całkiem poprawnym kodem Java, chociaż byłoby to łatwe do naprawienia. Właściwie to właśnie zamierzam zrobić w tej odpowiedzi!
Pozwól, że ci to krótko przeliteruję. Definiujemy…
typ rekurencyjny,
Tree<α>
który reprezentuje węzeł w drzewie binarnym. Każdy węzeł przechowujevalue
jakiś typ α i ma odniesienia do opcjonalnychleft
iright
poddrzew tego samego typu.funkcja,
height
która zwraca najdalszą odległość od dowolnego węzła liścia do węzła głównegot
.Teraz zamieńmy powyższy pseudokod
height
na właściwą składnię Java! (Ze względu na zwięzłość będę nadal pomijał pewne schematy, takie jak orientacja obiektowa i modyfikatory dostępności). Pokażę dwa możliwe rozwiązania.1. Rozwiązanie uniwersalne:
Najbardziej oczywistą poprawką jest po prostu utworzenie
height
generycznego poprzez wprowadzenie parametru typu α do jego podpisu:Umożliwiłoby to deklarowanie zmiennych i tworzenie wyrażeń typu α wewnątrz tej funkcji, gdybyś chciał. Ale...
2. Rozwiązanie typu egzystencjalnego:
Jeśli spojrzysz na treść naszej metody, zauważysz, że tak naprawdę nie mamy dostępu do niczego typu α ani nie pracujemy z nim ! Nie ma wyrażeń posiadających ten typ, ani żadnych zmiennych zadeklarowanych z tym typem ... więc dlaczego w ogóle musimy tworzyć
height
rodzajowe? Dlaczego nie możemy po prostu zapomnieć o α ? Jak się okazuje, możemy:Jak napisałem na samym początku tej odpowiedzi, typy egzystencjalne i uniwersalne mają charakter komplementarny / dualny. Tak więc, gdyby uniwersalne rozwiązanie typu miało uczynić
height
bardziej rodzajowym, to należałoby się spodziewać, że typy egzystencjalne mają odwrotny skutek: uczynienie go mniej rodzajowym, a mianowicie poprzez ukrycie / usunięcie parametru typu α .W konsekwencji nie można już odwoływać się do typu
t.value
w tej metodzie ani manipulować jakimikolwiek wyrażeniami tego typu, ponieważ żaden identyfikator nie został z nim powiązany. (Symbol?
wieloznaczny jest specjalnym tokenem, a nie identyfikatorem, który „przechwytuje” typ.) Wt.value
rzeczywistości stał się nieprzejrzysty; być może jedyne, co nadal możesz z nim zrobić, to przerzucić go na typObject
.Podsumowanie:
źródło
Object
jest dość interesujące: chociaż oba są podobne, ponieważ umożliwiają pisanie kodu statycznie niezależnego od typu, te pierwsze ( typy ogólne ) nie wyrzucają prawie wszystkich dostępnych informacji o typach do osiągnąć ten cel. W tym szczególnym sensie leki generyczne są lekarstwem naObject
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))); }
,E
touniversal type
i?
oznacza grupęexistential type
??
inint height(Tree<?> t)
nadal nie jest znany wewnątrz funkcji i nadal jest określany przez wywołującego, ponieważ to wywołujący musi wybrać, które drzewo ma przekazać. Nawet jeśli ludzie nazywają to typem egzystencjalnym w Javie, tak nie jest. W niektórych okolicznościach?
symbol zastępczy może służyć do zaimplementowania formy egzystencjalnej w Javie, ale nie jest to jedna z nich.To są dobre przykłady, ale wolę odpowiedzieć na to trochę inaczej. Przypomnijmy sobie z matematyki, że ∀x. P (x) oznacza „dla wszystkich x mogę udowodnić, że P (x)”. Innymi słowy, jest to rodzaj funkcji, dajesz mi x i mam metodę, aby to udowodnić.
W teorii typów nie mówimy o dowodach, ale o typach. Więc w tym miejscu mamy na myśli „dla każdego typu X, który mi podasz, podam ci konkretny typ P”. Ponieważ nie podajemy P zbyt wielu informacji na temat X poza faktem, że jest to typ, P nie może z nim wiele zrobić, ale jest kilka przykładów. P może tworzyć rodzaj „wszystkich par tego samego typu”:
P<X> = Pair<X, X> = (X, X)
. Lub możemy utworzyć typ opcjiP<X> = Option<X> = X | Nil
:, gdzie Nil jest typem wskaźników zerowych. Możemy zrobić listę z niego:List<X> = (X, List<X>) | Nil
. Zwróć uwagę, że ostatni jest rekurencyjny, wartościamiList<X>
są albo pary, gdzie pierwszy element to X, a drugi element to a,List<X>
albo jest to wskaźnik zerowy.Teraz w matematyce ∃x. P (x) oznacza „Mogę udowodnić, że istnieje taki konkretny x, że P (x) jest prawdziwy”. Takich x może być wiele, ale aby to udowodnić, wystarczy jeden. Inaczej można o tym myśleć, że musi istnieć niepusty zestaw par dowód-dowód {(x, P (x))}.
Przetłumaczone na teorię typów: typ w rodzinie
∃X.P<X>
to typ X i odpowiadający mu typP<X>
. Zauważ, że chociaż wcześniej daliśmy X do P (tak, że wiedzieliśmy wszystko o X, ale o P bardzo mało), teraz jest odwrotnie.P<X>
nie obiecuje podać żadnych informacji o X, tylko, że istnieje i że rzeczywiście jest typem.Jak to jest przydatne? Cóż, P może być typem, który ma sposób na ujawnienie swojego wewnętrznego typu X. Przykładem może być obiekt, który ukrywa wewnętrzną reprezentację swojego stanu X. Chociaż nie mamy możliwości bezpośredniego manipulowania nim, możemy zaobserwować jego efekt poprzez szturchanie w P. Może być wiele implementacji tego typu, ale można użyć wszystkich tych typów, niezależnie od tego, który z nich został wybrany.
źródło
P<X>
zamiast aP
(powiedzmy ta sama funkcjonalność i typ kontenera, ale nie wiesz, że zawieraX
)?∀x. P(x)
nie oznacza nic o możliwości udowodnieniaP(x)
, tylko prawdę.Aby bezpośrednio odpowiedzieć na Twoje pytanie:
W przypadku typu uniwersalnego użycie
T
musi zawierać parametr typeX
. Na przykładT<String>
lubT<Integer>
. W przypadku typów egzystencjalnych użycieT
nie obejmuje tego parametru, ponieważ jest on nieznany lub nieistotny - po prostu użyjT
(lub w JavieT<?>
).Dalsza informacja:
Typy uniwersalne / abstrakcyjne i typy egzystencjalne to dwoistość perspektywy między konsumentem / klientem obiektu / funkcji a jej producentem / wdrożeniem. Kiedy jedna strona widzi typ uniwersalny, druga widzi typ egzystencjalny.
W Javie możesz zdefiniować klasę ogólną:
MyClass
,T
jest uniwersalny, ponieważ można zastąpić dowolny typ dlaT
podczas korzystania z tej klasy, a trzeba wiedzieć, rzeczywisty typ T każdym użyciu instancjęMyClass
MyClass
samych metod instancjiT
jest egzystencjalny, ponieważ nie zna prawdziwego typuT
?
reprezentuje typ egzystencjalny - tak więc, gdy jesteś wewnątrz klasy, wT
zasadzie jest?
. Jeśli chcesz obsłużyć wystąpienieMyClass
zT
egzystencjalnym, możesz zadeklarowaćMyClass<?>
jak wsecretMessage()
powyższym przykładzie.Typy egzystencjalne są czasami używane do ukrywania szczegółów implementacji czegoś, co omówiono w innym miejscu. Wersja Java może wyglądać następująco:
Trochę trudno jest to poprawnie uchwycić, ponieważ udaję, że jestem w jakimś funkcjonalnym języku programowania, którym nie jest Java. Ale chodzi o to, że przechwytujesz jakiś stan oraz listę funkcji, które działają na tym stanie i nie znasz rzeczywistego typu części stanu, ale funkcje tak, ponieważ zostały już dopasowane do tego typu .
Teraz w Javie wszystkie nieokończone typy niebędące prymitywami są częściowo egzystencjalne. Może to zabrzmieć dziwnie, ale ponieważ zmienna zadeklarowana jako
Object
może potencjalnie być podklasąObject
zamiast tego, nie można zadeklarować określonego typu, a jedynie „ten typ lub podklasę”. I tak, obiekty są reprezentowane jako bit stanu oraz lista funkcji, które działają na tym stanie - dokładnie, która funkcja do wywołania jest określana w czasie wykonywania przez wyszukiwanie. Jest to bardzo podobne do użycia typów egzystencjalnych powyżej, w których mamy część stanu egzystencjalnego i funkcję, która działa na tym stanie.W językach programowania z typami statycznymi bez podtypów i rzutowań typy egzystencjalne pozwalają na zarządzanie listami obiektów o różnym typie. Lista
T<Int>
nie może zawieraćT<Long>
. Jednak listaT<?>
może zawierać dowolną odmianęT
, co pozwala na umieszczenie wielu różnych typów danych na liście i przekonwertowanie ich wszystkich na int (lub wykonanie jakichkolwiek operacji w strukturze danych) na żądanie.Prawie zawsze można przekonwertować rekord typu egzystencjalnego na rekord bez użycia domknięć. Zamknięcie jest również wpisane egzystencjalnie, ponieważ wolne zmienne, nad którymi jest zamykane, są ukryte przed wywołującym. Tak więc język, który obsługuje domknięcia, ale nie typy egzystencjalne, może pozwolić ci na tworzenie domknięć, które mają ten sam stan ukryty, który umieściłbyś w egzystencjalnej części obiektu.
źródło
Typ egzystencjalny to typ nieprzejrzysty.
Pomyśl o uchwycie pliku w systemie Unix. Wiesz, że jest to typ int, więc możesz go łatwo podrobić. Możesz na przykład spróbować czytać z uchwytu 43. Jeśli tak się stanie, że program ma otwarty plik z tym konkretnym uchwytem, będziesz czytać z niego. Twój kod nie musi być złośliwy, po prostu niechlujny (np. Uchwyt może być niezainicjowaną zmienną).
Typ egzystencjalny jest ukryty przed twoim programem. Jeśli
fopen
zwracany jest typ egzystencjalny, wszystko, co możesz z nim zrobić, to użyć go z niektórymi funkcjami bibliotecznymi, które akceptują ten typ egzystencjalny. Na przykład skompilowałby się następujący pseudokod:Interfejs „do odczytu” jest deklarowany jako:
Istnieje typ T taki, że:
Zmienna exfile nie jest int, nie jest
char*
ani strukturą File - nic, co można wyrazić w systemie typów. Nie możesz zadeklarować zmiennej, której typ jest nieznany i nie możesz rzutować, powiedzmy, wskaźnika na ten nieznany typ. Język ci na to nie pozwala.źródło
read
jest,∃T.read(T file, ...)
wtedy nie ma nic, co można by przekazać jako pierwszy parametr. Co by zadziałało, tofopen
zwrócenie uchwytu pliku i funkcji odczytu w zakresie tego samego egzystencjalnego zakresu :∃T.(T, read(T file, ...))
Wygląda na to, że spóźniam się trochę, ale w każdym razie ten dokument dodaje inny pogląd na to, jakie typy egzystencjalne są, chociaż nie jest to specyficznie niezależne od języka, powinno być wtedy znacznie łatwiejsze do zrozumienia typów egzystencjalnych: http: //www.cs.uu .nl / groups / ST / Projects / ehc / ehc-book.pdf (rozdział 8)
źródło
Typ uniwersalny istnieje dla wszystkich wartości parametrów typu. Typ egzystencjalny istnieje tylko dla wartości parametrów typu, które spełniają ograniczenia typu egzystencjalnego.
Na przykład w Scali jednym ze sposobów wyrażenia typu egzystencjalnego jest typ abstrakcyjny, który jest ograniczony do jakichś górnych lub dolnych granic.
Równoważnie ograniczony typ uniwersalny jest typem egzystencjalnym, jak w poniższym przykładzie.
Każda witryna użytkowania może stosować,
Interface
ponieważ wszystkie instancyjne podtypyExistential
muszą definiować,type Parameter
które muszą implementowaćInterface
.Zdegenerowany przypadek od typu egzystencjalnego w Scala jest abstrakcyjny typ, który nigdy nie jest dalej, a zatem nie musi być określone przez każdego podtypu. To faktycznie ma skróconą notację
List[_]
w Scali iList<?>
Javie.Moja odpowiedź została zainspirowana propozycją Martina Odersky'ego, aby ujednolicić typy abstrakcyjne i egzystencjalne. Slajdów towarzyszący wspomaga zrozumienie.
źródło
∀x.f(x)
są nieprzejrzyste dla swoich funkcji odbiorczych, podczas gdy typy egzystencjalne∃x.f(x)
są ograniczone do posiadania pewnych właściwości. Zwykle wszystkie parametry są egzystencjalne, ponieważ ich funkcją będzie manipulowanie nimi bezpośrednio; jednak parametry ogólne mogą mieć typy, które są uniwersalne, ponieważ funkcja nie będzie nimi zarządzać poza podstawowymi operacjami uniwersalnymi, takimi jak uzyskiwanie odniesienia, jak w:∀x.∃array.copy(src:array[x] dst:array[x]){...}
forSome
kwantyfikacja egzystencjalna parametru typu.Badania nad abstrakcyjnymi typami danych i ukrywaniem informacji wprowadziły typy egzystencjalne do języków programowania. Tworzenie abstraktu typu danych ukrywa informacje o tym typie, więc klient tego typu nie może ich nadużywać. Powiedzmy, że masz odniesienie do obiektu ... niektóre języki umożliwiają rzutowanie tego odniesienia na odniesienie do bajtów i robienie wszystkiego, co chcesz, z tym fragmentem pamięci. W celu zagwarantowania zachowania programu przydatne jest, aby język wymusił, że działasz tylko na odwołaniu do obiektu za pośrednictwem metod dostarczonych przez projektanta obiektu. Wiesz, że ten typ istnieje, ale nic więcej.
źródło
Stworzyłem ten diagram. Nie wiem, czy to jest rygorystyczne. Ale jeśli to pomoże, cieszę się.
źródło
Jak rozumiem, jest to matematyczny sposób opisania interfejsów / klasy abstrakcyjnej.
Jak dla T = ∃X {X a; int f (X); }
W przypadku C # będzie to przekładać się na ogólny typ abstrakcyjny:
„Egzystencjalny” oznacza po prostu, że istnieje pewien typ, który jest zgodny z określonymi tutaj regułami.
źródło