Jeśli coś jest proste, powinno być całkowicie wyjaśnione kilkoma słowami. Można to zrobić dla rachunku λ:
Rachunek λ jest gramatyką składniową (w zasadzie strukturą) z regułą redukcji (co oznacza, że procedura wyszukiwania / zamiany jest wielokrotnie stosowana do każdego wystąpienia określonego wzorca, dopóki taki wzorzec nie istnieje).
Gramatyka:
Term = (Term Term) | (λ Var . Term) | Var
Reguła redukcji:
((λ var body) term) -> SUBS(body,var,term) where `SUBS` replaces all occurrences of `var` by `term` in `body`, avoiding name capture.
Przykłady:
(λ a . a) -> (λ a a) ((λ a . (λ b . (b a))) (λ x . x)) -> (λ b . (b (λ x x))) ((λ a . (a a)) (λ x . x)) -> (λ x . x) ((λ a . (λ b . ((b a) a))) (λ x . x)) -> (λ b . ((b (λ x . x)) (λ x . x))) ((λ x . (x x)) (λ x . (x x))) -> never halts
Chociaż jest to nieco nieformalne, można argumentować, że jest to wystarczająco pouczające, aby normalny człowiek zrozumiał rachunek λ jako całość - i wymaga 22 linii przeceny. Próbuję zrozumieć systemy typu czystego / zależnego używane przez Idris / Agda i podobne projekty, ale krótszym wyjaśnieniem, które mogłem znaleźć, było po prostu łatwe - świetny artykuł, ale wydaje się, że zakłada on dużo wcześniejszej wiedzy (Haskell, indukcyjny definicje), których nie mam. Myślę, że coś krótszego, mniej bogatego może wyeliminować niektóre z tych barier. A zatem,
Czy można podać krótkie, pełne wyjaśnienie układów typu czystego / zależnego, w tym samym formacie, w którym przedstawiłem rachunek λ powyżej?
źródło
Odpowiedzi:
Zrzeczenie się
Jest to bardzo nieformalne, jak prosiłeś.
Gramatyka
W języku zależnym od typu mamy spoiwo na poziomie typu, a także na poziomie wartości:
Dobrze wpisany termin to termin z załączonym typem, napiszemy
t ∈ σ
lubwskazując, że termin
t
ma typσ
.Zasady pisania
Dla uproszczenia wymagamy tego w
λ v. t ∈ ∀ (v : σ). τ
obu przypadkachλ
i∀
wiążemy tę samą zmienną (v
w tym przypadku).Zasady:
Zatem
*
„jest typem wszystkich typów” (1),∀
tworzy typy z typów (2), abstrakcje lambda mają typy pi (3), a jeśliv
wprowadza je∀ (v : σ). τ
, tov
ma typσ
(5).„w normalnej formie” oznacza, że wykonujemy możliwie najwięcej redukcji, stosując zasadę redukcji:
„Reguła redukcji”
Lub w dwuwymiarowej składni gdzie
oznacza
t ∈ σ
:Można zastosować abstrakcję lambda do terminu, gdy termin ma ten sam typ co zmienna w powiązanym kwantyfikatorze. Następnie zmniejszamy zarówno abstrakcję lambda, jak i kwantyfikator forall w taki sam sposób, jak w czystym rachunku lambda wcześniej. Po odjęciu części poziomu wartości otrzymujemy regułę pisania (4).
Przykład
Oto operator aplikacji funkcji:
(możemy skrócić
∀ (x : σ). τ
doσ -> τ
jeżeliτ
nie wspominającx
)f
zwracaB y
dla dowolnego podanegoy
typuA
. Stosujemyf
sięx
, który jest odpowiedni rodzajA
, a substytutemy
dlax
w∀
PO.
, więcf x ∈ SUBS(B y, y, x)
~>f x ∈ B x
.Skróćmy teraz operatora aplikacji funkcji jako
app
i zastosujmy go do siebie:Stawiam
?
na warunki, które musimy podać. Najpierw wyraźnie wprowadzamy i tworzymy wystąpieniaA
orazB
:Teraz musimy ujednolicić to, co mamy
który jest taki sam jak
i co
app ? ?
otrzymujeTo skutkuje
(patrz także Co to jest predykatywność? )
Nasze wyrażenie (po zmianie nazwy) staje się
Ponieważ dla każdego
A
,B
if
(gdzief ∈ ∀ (y : A). B y
)możemy utworzyć
A
iB
uzyskać (dla każdegof
z odpowiednim typem)a podpis typu jest równoważny
(∀ (x : A). B x) -> ∀ (x : A). B x
.Całe wyrażenie brzmi
To znaczy
co po wszystkich obniżkach na poziomie wartości daje to samo
app
.Więc choć wymaga to zaledwie kilka kroków w czystym rachunkiem lambda, aby uzyskać
app
odapp app
, w typie ustawienia (a zwłaszcza w sposób zależny od wpisane) musimy także dbać o unifikacji i rzeczy stają się bardziej złożone nawet z pewnym wygody inconsitent (* ∈ *
).Sprawdzanie typu
t
jest*
następniet ∈ *
przez (1)t
jest∀ (x : σ) τ
,σ ∈? *
,τ ∈? *
(patrz uwaga na temat∈?
poniżej), a następniet ∈ *
przez (2)t
jestf x
,f ∈ ∀ (v : σ) τ
aby częśćσ
, aτ
,x ∈? σ
a następniet ∈ SUBS(τ, v, x)
przez (4)t
jest to zmiennav
,v
wprowadzona została∀ (v : σ). τ
następniet ∈ σ
przez (5)Wszystkie są regułami wnioskowania, ale nie możemy zrobić tego samego dla lambdas (wnioskowanie typu jest nierozstrzygalne dla typów zależnych). Więc dla lambdas sprawdzamy (
t ∈? σ
) zamiast wnioskować:t
jestλ v. b
i sprawdzone przeciwko∀ (v : σ) τ
,b ∈? τ
tot ∈ ∀ (v : σ) τ
t
jest coś innego i sprawdzone w stosunku doσ
tego, wywnioskuj rodzajt
użycia powyższej funkcji i sprawdź, czy tak jestσ
Sprawdzanie równości typów wymaga, aby były one w normalnej formie, więc aby zdecydować, czy
t
ma typσ
, najpierw sprawdzamy, czyσ
ma typ*
. Jeśli tak, toσ
jest normalizowalny (paradoks modulo Girarda) i normalizuje się (stądσ
staje się dobrze uformowany przez (0)).SUBS
normalizuje również wyrażenia do zachowania (0).Nazywa się to dwukierunkowym sprawdzaniem typu. Dzięki niemu nie musimy dodawać adnotacji do każdej lambda typem: jeśli w
f x
typief
jest znany, tox
jest sprawdzany pod kątem rodzaju argumentuf
otrzymywanego zamiast wnioskowania i porównywania pod kątem równości (co jest również mniej wydajne). Ale jeślif
jest lambda, wymaga wyraźnej adnotacji typu (adnotacje są pomijane w gramatyce i wszędzie można dodaćAnn Term Term
lubλ' (σ : Term) (v : Var)
do konstruktorów).Rzuć też okiem na prostsze, łatwiejsze! post na blogu.
źródło
(λ v. b ∈ ∀ (v : σ). τ) (t ∈ σ)
~>SUBS(b, v, t) ∈ SUBS(τ, v, t)
.(∀ (v : σ). τ) t ~> ...
, drugą dla znaczących(λ v. b) t ~> ...
. Chciałbym usunąć pierwszy i zamienić go w komentarz poniżej.Chodźmy. Nie zawracam sobie głowy paradoksem Girarda, ponieważ odwraca on uwagę od głównych idei. Będę musiał przedstawić maszynerię prezentacyjną na temat osądów i pochodnych i tym podobnych.
Gramatyka
Gramatyka ma dwie wzajemnie zdefiniowane formy, „terminy”, które są ogólnym pojęciem rzeczy (typy są rzeczami, wartości są rzeczami), w tym * (typ typów), typy funkcji zależnych i abstrakty lambda, ale także osadzają „ eliminacje ”(tzn.„ zastosowania ”zamiast„ konstrukcje ”), które są zagnieżdżonymi aplikacjami, w których rzecz ostatecznie w pozycji funkcji jest albo zmienną, albo terminem opatrzonym adnotacją ze swoim typem.
Zasady redukcji
Operacja podstawienia t [e / x] zastępuje każde wystąpienie zmiennej x eliminacją e, unikając przechwytywania nazw. Aby utworzyć aplikację, która może zredukować, termin lambda musi być opatrzony adnotacjami według typu, aby wyeliminować . Adnotacja typu nadaje abstrakcji lambda rodzaj „reaktywności”, umożliwiając kontynuowanie aplikacji. Gdy osiągniemy punkt, w którym nie będzie już więcej aplikacji, a aktywny t: T zostanie ponownie osadzony w składni terminu, możemy usunąć adnotację typu.
Rozszerzmy relację redukcji by o zamknięcie strukturalne: zasady obowiązują w dowolnym miejscu warunków i eliminacji, że można znaleźć coś pasującego do lewej strony. Napisz ↝ * dla zwrotno-przechodniego (0-lub więcej-stopniowego) zamknięcia ↝. Powstały system redukcji jest zbieżny w tym sensie:
Konteksty
Konteksty to sekwencje, które przypisują typy do zmiennych, rosnące po prawej stronie, które uważamy za „lokalny” koniec, mówiąc nam o ostatnio powiązanych zmiennych. Ważną właściwością kontekstów jest to, że zawsze można wybrać zmienną, która nie jest jeszcze używana w kontekście. Zachowujemy niezmienność, że zmienne przypisane typy w kontekście są różne.
Wyroki
To jest gramatyka wyroków, ale jak je czytać? Na początek ⊢ jest tradycyjnym symbolem „kołowrotu”, oddzielającym założenia od wniosków: można je odczytać nieformalnie jako „mówi”.
oznacza, że w kontekście G typ T przyjmuje termin t;
oznacza, że w kontekście G, eliminacja e ma typ S.
Sądy mają interesującą strukturę: zero lub więcej danych wejściowych , jeden lub więcej tematów , zero lub więcej danych wyjściowych .
Oznacza to, że musimy z góry zaproponować rodzaje terminów i po prostu je sprawdzić , ale syntezujemy rodzaje eliminacji.
Zasady pisania
Przedstawiam je w nieco niejasnym stylu prologicznym, pisząc J -: P1; ...; Pn, aby wskazać, że orzeczenie J obowiązuje, jeśli przesłanki od P1 do Pn również mają zastosowanie. Przesłanką będzie kolejny wyrok lub roszczenie o obniżkę.
I to wszystko!
Tylko dwie reguły nie są ukierunkowane na składnię: reguła, która mówi „możesz zredukować typ, zanim użyjesz go do sprawdzenia terminu” oraz reguła, która mówi „możesz zredukować typ po zsyntetyzowaniu go z eliminacji”. Jedną z realnych strategii jest redukcja typów, dopóki nie ujawnisz najwyższego konstruktora.
Ten system nie jest silnie normalizujący (z powodu paradoksu Girarda, który jest paradoksem odwoływania się do kłamstwa), ale można go silnie normalizować, dzieląc * na „poziomy wszechświata”, gdzie wszelkie wartości, które dotyczą typów na niższych poziomach, same mają typy na wyższych poziomach, co zapobiega odwoływaniu się do siebie.
Ten system ma jednak w tym sensie właściwość zachowania typu.
Konteksty można obliczać, umożliwiając obliczenie zawartych w nich terminów. Oznacza to, że jeśli orzeczenie jest teraz ważne, możesz obliczyć jego dane wejściowe tak, jak chcesz, i jeden temat, a następnie będzie można jakoś obliczyć jego dane wyjściowe, aby upewnić się, że wynikowy wyrok pozostanie ważny. Dowodem jest prosta indukcja na temat pisania na klawiaturze, biorąc pod uwagę zbieżność -> *.
Oczywiście przedstawiłem tutaj tylko rdzeń funkcjonalny, ale rozszerzenia mogą być dość modułowe. Oto pary.
źródło
G, x:S, G' ⊢ x is S -: G' ⊬ x
?Korespondencja Curry-Howarda mówi, że istnieje systematyczna zgodność między systemami typów i systemami dowodzenia w logice. Przyjmując to zorientowane na programistę, możesz przekształcić to w następujący sposób:
Patrząc pod tym kątem:
Oto zasady naturalnego odliczenia dla logiki pierwszego rzędu, przy użyciu diagramu z pozycji Wikipedii na temat naturalnego odliczenia . Są to w zasadzie także zasady minimalnie zależnego rachunku lambda.
Pamiętaj, że reguły zawierają wyrażenia lambda. Można je odczytać jako programy, które konstruują dowody zdań reprezentowanych przez ich typy (lub, mówiąc bardziej zwięźle, mówimy, że programy są dowodami ). Podobne zasady redukcji, które podajesz, można zastosować do tych warunków lambda.
Dlaczego nam na tym zależy? Po pierwsze, ponieważ proofy mogą okazać się przydatnym narzędziem w programowaniu, a posiadanie języka, który może współpracować z proofami, ponieważ obiekty pierwszej klasy otwierają wiele możliwości. Na przykład, jeśli twoja funkcja ma warunek wstępny, zamiast zapisywać ją jako komentarz, możesz faktycznie zażądać jej dowodu jako argumentu.
Po drugie, ponieważ maszyneria systemu typów potrzebna do obsługi kwantyfikatorów może mieć inne zastosowania w kontekście programowania. W szczególności języki o typie zależnym obsługują uniwersalne kwantyfikatory („dla wszystkich x, ...”) za pomocą pojęcia zwanego typami funkcji zależnych - funkcją, w której typ statyczny wyniku może zależeć od wartości czasu wykonywania argumentu.
Aby nadać temu zastosowanie bardzo pieszo, piszę cały czas kod, który musi czytać pliki Avro , które składają się z rekordów o jednolitej strukturze - wszystkie mają ten sam zestaw nazw i typów pól. Wymaga to ode mnie:
Jak widać na stronie samouczka Avro Java , pokazują one, jak korzystać z biblioteki zgodnie z obydwoma tymi podejściami.
Z zależnymi typami funkcji możesz mieć ciasto i zjeść je, kosztem bardziej złożonego systemu typów. Możesz napisać funkcję, która odczytuje plik Avro, wyodrębnia schemat i zwraca zawartość pliku jako strumień rekordów, których typ statyczny zależy od schematu przechowywanego w pliku . Kompilator byłby w stanie wychwycić błędy, w których na przykład próbowałem uzyskać dostęp do nazwanego pola, które może nie istnieć w rekordach plików, które będzie przetwarzane w czasie wykonywania. Kochanie, co?
źródło