D2z: Dobre, złe i podejrzane typy induktywne


Require Import Arith.

From Typonomikon Require Import BC1c.

Poznana przez nas dotychczas definicja typów induktywnych (oraz wszelkich ich ulepszeń, jak indukcja-indukcja, indukcja-rekursja etc.) jest niepełna. Tak jak świat pełen jest złoczyńców oszukujących starszych ludzi metodą "na wnuczka", tak nie każdy typ podający się za induktywny faktycznie jest praworządnym obywatelem krainy typów induktywnych.
Na szczęście typy induktywne to istoty bardzo prostolinijne, zaś te złe można odróżnić od tych dobrych gołym okiem, za pomocą bardzo prostego kryterium: złe typy induktywne to te, które nie są ściśle pozytywne. Zanim jednak dowiemy się, jak rozpoznawać złe typy induktywne, poznajmy najpierw dwa powody, przez które złe typy induktywne są złe.
Przyjrzyjmy się poniższemu typowemu przypadkowi negatywnego typu induktywnego (co dokładnie znaczy w tym kontekście słowo "negatywny" i jak takie typy rozpoznawać zobaczymy później):

Fail Inductive wut (A : Type) : Type :=
| C : (wut A -> A) -> wut A.

(* ===> The command has indeed failed with message:
        Non strictly positive occurrence of "wut"
        in "(wut A -> A) -> wut A". *)


Uwaga: poprzedzenie komendą Fail innej komendy oznajmia Coqowi, że spodziewamy się, iż komenda zawiedzie. Coq akceptuje komendę Fail c, jeżeli komenda c zawodzi, i wypisuje jej komunikat o błędzie. Jeżeli komenda c zakończy się sukcesem, komenda Fail c zwróci błąd. Komenda Fail jest przydatna w sytuacjach takich jak obecna, gdy chcemy zilustrować fakt, że jakaś komenda zawodzi.
W naszym przypadku komenda Fail kończy się sukcesem, a zatem próba zdefiniowania powyższego typu induktywnego się nie powiodła. Wiadomość o błędzie podaje nam, jak na tacy, powód tej sytuacji: typ konstruktora C zawiera nie-ściśle-pozytywne wystąpienie definiowanego typu wut A.
Komenda Fail c ma też jednak pewne wady: poza poświadczeniem rezultatu wykonania komendy c, nie ma ona żadnych innych skutków. To sprawia, że użycie Fail zmusiłoby nas, w dalszej części podrozdziału, do zaledwie udawania, że mamy jakiś typ i coś z nim robimy. To zaś jest bardzo złe, bo bez czujnego Coqowego oka bardzo łatwo jest napisać coś głupiego lub popełnić jakiś inny błąd.

Module wut.

Unset Positivity Checking.
Inductive wut (A : Type) : Type :=
| C : (wut A -> A) -> wut A.
Set Positivity Checking.

Na szczęście jest sposób na to, byśmy mogli pobawić się nie-ściśle-pozytywnymi typami induktywnymi pod czujnym okiem Coqa: posługując się komendą Unset Positivity Checking możemy wyłączyć positivity checker (czyli po polsku "sprawdzacz pozytywności"), co sprawi, że Coq zaakceptuje definicję typu wut. Dzięki temu będziemy mogli posługiwać się tym typem jak gdyby nigdy nic.
Oczywiście takie rozwiązanie również niesie za sobą negatywne konsekwencje: jak za chwilę zobaczymy, z istnienia typu wut można wywnioskować dowód fałszu, a zatem od teraz możemy udowodnić w Coqu dosłownie wszystko, więc teoretycznie wszystkie nasze dowody stają się bezwartościowe. W praktyce rzecz jasna nie będziemy tej sprzeczności wykorzystywać w niecnych celach, a istnienie typu wut A dopuszczamy tylko w celach ilustracyjnych.

Nieterminacja jako źródło zła na świecie

Pierwszym powodem nielegalności nie-ściśle-pozytywnych typów induktywnych jest to, że unieważniają one filozoficzną interpretację teorii typów i pozwalają łamać reguły dzięki którym to co robimy w Coqu ma jakikolwiek sens (co jednak tylko czasami prowadzi do sprzeczności bezpośrednio). Przyjrzyjmy się poniższemu programowi:

Definition loop (A : Type) : A :=
  let f (w : wut A) : A :=
    match w with
    | C _ g => g w
    end
  in f (C _ f).

Już sam typ tego programu wygląda złowrogo: dla każdego typu A program zwraca element typu A. Nie dziwota więc, że możemy uzyskać z niego dowód fałszu wstawiając False za A.

Definition santa_is_a_pedophile : False := loop False.

Paradoksalnie jednak to nie ta rażąca sprzeczność jest naszym największym problemem - nie z każdego złego typu induktywnego da się tak łatwo dostać sprzeczność (systematyczny sposób dostawania sprzeczności z istnienia takich typów zobaczymy później). W rzeczywistości jest nim nieterminacja.
Nieterminacja (ang. nontermination) lub kolokwialniej zapętlenie to sytuacja, w której program nigdy nie skończy się wykonywać. Ta właśnie bolączka jest przypadłością loop, czego nietrudno domyślić się po nazwie.
Dlaczego loop nie terminuje? Przyjrzyjmy się definicji: za pomocą leta definiujemy funkcję f : wut A -> A, która odpakowuje swój argument w, wyciąga z niego funkcję g : wut A -> A i aplikuje g do w, czyli do oryginalnego argumentu f. Wynikiem całego programu jest f zaaplikowane do siebie samego zawiniętego w konstruktor C (żeby typy się zgadzały).
Ta aplikacja czegoś do samego siebie to kolejny sygnał ostrzegawczy, który powinien wzbudzić naszą czujność. Ogólniej sytuacja, w której coś odnosi się samo do siebie, nazywa się autoreferencją i często prowadzi do różnych wesołych paradoksów.
Żeby jeszcze lepiej zrozumieć nieterminację loop, spróbujmy interaktywnie prześledzić, w jaki sposób program ten się oblicza.

Goal loop unit = tt.
Proof.
  cbv delta [loop].
  cbv beta.
  cbv zeta.

  cbv beta. cbv iota.
  cbv beta. cbv iota.
  cbv beta; cbv iota.
Abort.

Jeżeli jesteś leniwy i nie masz włączonego CoqIDE, sprowadza się to do czegoś w tym stylu:
loop A =
let f := ... in f (C f) =
let f := ... in match C f with | C g => g (C f) end =
let f := ... in f (C f)
i tak dalej.
To natomiast, co powinno nas tu zdziwić, to fakt, że loop jest funkcją w pewnym sensie rekurencyjną (bo funkcja f wywołuje sama siebie), mimo że przecież nie użyliśmy komendy Fixpoint!
To jest właśnie jeden z przejawów zamętu, jaki wprowadzają negatywny typy induktywne - zmieniają reguły gry. Dotychczas Fixpoint (i fix) dawały nam możliwość użycia rekursji (a w praktyce oznaczały, że rekursji faktycznie użyliśmy), zaś Definition (i fun) świadczyło o tym, że funkcja nie jest rekurencyjna. Od kiedy tylko Coq zaakceptował definicję typu wut A, regułę tę można bez przeszkód łamać, z opłakanymi konsekwencjami.
Przyjrzyjmy się teraz problemom filozoficznym powodowanym przez nieterminację. W skrócie: zmienia ona fundamentalne właściwości obliczeń, co prowadzi do zmiany interpretacji pojęcia typu, zaś to pociąga za sobą kolejne przykre skutki, takie jak np. to, że reguły eliminacji tracą swoje uzasadnienie. Brzmi mega groźnie, prawda?
Na szczęście tak naprawdę, to sprawa jest prosta. W Coqu wymagamy, aby każde obliczenie się kończyło. Końcowe wyniki obliczeń (zwane też wartościami, postaciami kanonicznymi lub postaciami normalnymi) możemy utożsamiać z elementami danego typu. Dla przykładu wynikami obliczania termów typu booltrue i false, więc możemy myśleć, że są to elementy typu bool i bool składa się tylko z nich. To z kolei daje nam uzasadnienie reguły eliminacji (czyli indukcji) dla typu bool: żeby udowodnić P : bool -> Prop dla każdego b : bool, wystarczy udowodnić P true i P false, gdyż true i false są jedynymi elementami typu bool.
Nieterminacja obraca tę jakże piękną filozoficzną wizję w perzynę: nie każde obliczenie się kończy, a przez to powstają nowe, "dziwne" elementy różnych typów. loop bool nigdy nie obliczy się do true ani do false, więc możemy traktować je jako nowy element typu bool. To sprawia, że bool, typ z założenia dwuelementowy, ma teraz co najmniej trzy elementy - true, false i loop bool. Z tego też powodu reguła eliminacji przestaje obowiązywać, bo wymaga ona dowodów jedynie dla true i false, ale milczy na temat loop bool. Moglibyśmy próbować naiwnie ją załatać, uwzględniając ten dodatkowy przypadek, ale tak po prawdzie, to nie wiadomo nawet za bardzo jakie jeszcze paskudztwa rozpanoszyły się w typie bool z powodu nieterminacji.
Morał jest prosty: nieterminacja to wynalazek szatana, a negatywne typy induktywne to też wynalazek szatana.

Ćwiczenie

Użyj typu wut nat żeby zdefiniować nieskończoną liczbę naturalną omega (jeżeli szukasz inspiracji, zerknij na definicję loop). Następnie udowodnij, że omega jest większa od każdej innej liczby naturalnej.


Lemma lt_n_omega :
  forall n : nat, n < omega.

Ćwiczenie

Jakie elementy ma typ nat? Wypisz kilka początkowych, a potem opisz ogólną zasadę ich powstawania.
Napisz jakiś term t : nat, który nie jest wartością, ale terminuje. Jaki jest wynik obliczenia tego termu?
Następnie przyjrzyj się termowi loop nat. Czy term ten terminuje? Pobaw się nim w trybie dowodzenia, żeby się przekonać (że nie, bo niby czego innego się spodziewałeś?).
Przypomnij sobie regułę indukcji dla nat. Gdyby udowodnić przez indukcję forall n : nat, P n, jak wygląda dowód P t, gdzie t to term, który napisałeś powyżej? A jak wygląda dowód P (loop nat)?

Ćwiczenie

Spróbujmy lepiej zrozumieć, o co chodzi z tym, że reguły eliminacji "tracą swoje uzasadnienie": udowodnij, że dla każdej liczby naturalnej istnieje liczba od niej większa (zdaje się, że już to przerabialiśmy).

Lemma no_greatest :
  forall n : nat, exists m : nat, n < m.

Jak to możliwe, że udało nam się udowodnić to twierdzenie, skoro przecież w poprzedinm ćwiczeniu udowodniliśmy, że największą liczbą naturalną jest omega?
Ano, omega jest niestandardową liczbą naturalną - pomiotem szatana powstałym w wyniku nieterminacji - i w związku z tym indukcja milczy na jej temat. Parafrazując: żeby udowodnić coś dla wszystkich liczb naturalnych przez indukcję, w ogóle nie musimy przejmować się omegą. Chyba nie trzeba dodawać, że to kolejna droga prowadząca prosto do sprzeczności?

Lemma yes_and_no : False.
Proof.
  destruct (no_greatest omega) as [n H1].
  assert (H2 := lt_n_omega n).
  apply Nat.lt_asymm in H1.
  contradiction.
Qed.

End wut.

Żeby przez przypadek nie użyć żadnej ze sprzeczności, które daje nam typ wut, schowaliśmy je w osobnym module, też nazwanym wut. Oczywiście wciąż mamy do nich dostęp, ale teraz ciężej będzie nam się pomylić.

Twierdzenie Cantora

Zanim zaczniemy ten podrozdział na poważnie, mam dla ciebie wesoły łamaniec językowy:
Cantor - kanciarz, który skradł zza kurtyny kantoru z Kantonu kontury kartonu Koranicznemu kanarowi, który czasem karał karczystych kafarów czarami za karę za kantowanie i za zakatowanie zza kontuaru konarem kontrkulturowych kuluarowych karłów.
Dobra, wystarczy. Reszta tego podrozdziału będzie śmiertelnie poważna, a przyjrzymy się w niej jednemu z mega klasycznych twierdzeń z końca XIX w. głoszącemu mniej więcej, że "zbiór potęgowy zbioru liczb naturalnych ma większą moc od zbioru liczb naturalnych".
Co za bełkot, pomyślisz zapewne. Ten podrozdział poświęcimy właśnie temu, żeby ów bełkot nieco wyklarować. Jeżeli zaś zastanawiasz się, po co nam to, to odpowiedź jest prosta - na (uogólnionym) twierdzeniu Cantora opierać się będzie nasza systematyczna metoda dowodzenia nielegalności negatywnych typów induktywnych.
Oczywiście oryginalne sformułowanie twierdzenia powstało na długo przed powstaniem teorii typów czy Coqa, co objawia się np. w tym, że mówi ono o zbiorze liczb naturalnych, podczas gdy my dysponujemy typem liczb naturalnych. Musimy więc oryginalne sformułowanie lekko przeformułować, a także wprowadzić wszystkie niezbędne nam pojęcia.

Definition surjective {A B : Type} (f : A -> B) : Prop :=
  forall b : B, exists a : A, f a = b.

Pierwszym niezbędnym pojęciem jest pojęcie surjekcji. Powyższa definicja głosi, że funkcja jest surjektywna, gdy każdy element przeciwdziedziny jest wynikiem funkcji dla pewnego elementu dziedziny. Surjekcja to funkcja, która jest surjektywna.
O co chodzi w tej definicji? Samo słowo "surjekcja" pochodzi od fr. "sur" - "na" oraz łac. "iacere" - "rzucać". Ubogim tłumaczeniem na polski może być słowo "narzut".
Idea jest taka, że surjekcja f : A -> B "narzuca" swoją dziedzinę A na przeciwdziedzinę B tak, że A "pokrywa" całe B. Innymi słowy, każdy element b : B jest pokryty przez jakiś element a : A, co wyraża równość f a = b. Oczywiście to a nie musi być unikalne - b może być pokrywane przez dużo różnych a. Jak widać, dokładnie to jest napisane w powyższej definicji.
Mówiąc jeszcze prościej: jeżeli f : A -> B jest surjekcją, to typ A jest większy (a precyzyjniej mówiący, nie mniejszy) niż typ B. Oczywiście nie znaczy to, że jeżeli f nie jest surjekcją, to typ A jest mniejszy niż B - mogą przecież istnieć inne surjekcje. Powiemy, że A jest mniejszy od B, jeżeli nie istnieje żadna surjekcja między nimi.
Spróbujmy rozrysować to niczym Jacek Gmoch... albo nie, bo nie umiem jeszcze rysować, więc zamiast tego będzie przykład i ćwiczenie.

Definition isZero (n : nat) : bool :=
match n with
| 0 => true
| _ => false
end.

Lemma surjective_isZero : surjective isZero.
Proof.
  unfold surjective. destruct b.
    exists 0. cbn. reflexivity.
    exists 42. cbn. reflexivity.
Qed.

Funkcja isZero, która sprawdza, czy jej argument jest zerem, jest surjekcją, bo każdy element typu bool może być jej wynikiem - true jest wynikiem dla 0, zaś false jest jej wynikiem dla wszystkich innych argumentów. Wobec tego możemy skonkludować, że typ nat jest większy niż typ bool i w rzeczywistości faktycznie tak jest: bool ma dwa elementy, a nat nieskończenie wiele.
Do kwestii tego, który typ ma ile elementów wrócimy jeszcze w rozdziale o typach i funkcjach. Tam też zapoznamy się lepiej z surjekcjami i innymi rodzajami funkcji. Tymczasem:

Ćwiczenie

Czy funkcja plus 5 jest surjekcją? A funkcja fun n : nat => minus n 5? Sprawdź swoje odpowiedzi w Coqu. Na koniec filozoficznie zinterpretuj otrzymany wynik.
Wskazówka: minus to funkcja z biblioteki standardowej, która implementuje odejmowanie liczb naturalnych z obcięciem, czyli np. 2 - 5 = 0. Użyj Printa, żeby dokładnie zbadać jej definicję.


Pozostaje jeszcze kwestia tego, czym jest "zbiór potęgowy zbioru liczb naturalnych". Mimo groźnej nazwy sprawa jest prosta: jest to archaiczne określenie na typ funkcji nat -> bool. Każdą funkcję f : nat -> bool możemy interpretować jako pewną kolekcję (czyli właśnie zbiór) elementów typu nat, zaś f n, czyli wynik f na konkretnym n, mówi nam, czy n znajduje się w tej kolekcji, czy nie.
To w zasadzie wyczerpuje zestaw pojęć potrzebnych nam do sformułowania twierdzenia. Pojawiająca się w oryginalnej wersji "większa moc" to po prostu synonim określenia "większy", które potrafimy już wyrażać za pomocą pojęcia surjekcji. Tak więc nowszą (czyli bardziej postępową) wersję twierdzenia Cantora możemy sformułować następująco: nie istnieje surjekcja z nat w nat -> bool. Lub jeszcze bardziej obrazowo: nat jest mniejsze niż nat -> bool.

Lemma Cantor :
  forall f : nat -> (nat -> bool), ~ surjective f.
Proof.
  unfold surjective. intros f Hf.
  pose (diagonal := fun n : nat => negb (f n n)).
  destruct (Hf diagonal) as [n Hn].
  apply (f_equal (fun h : nat -> bool => h n)) in Hn.
  unfold diagonal in Hn. destruct (f n n); inversion Hn.
Qed.


Dowód twierdzenia jest równie legendarny jak samo twierdzenie, a na dodatek bajecznie prosty i niesamowicie użyteczny - jeżeli będziesz zajmował się w życiu matematyką i informatyką, spotkasz go w jeszcze wielu odsłonach. Metoda stojąca za dowodem nazywana bywa argumentem przekątniowym - choć nazwa ta może się wydawać dziwna, to za chwilę stanie się zupełnia jasna.
O co więc chodzi w powyższym dowodzie? Po pierwsze zauważmy, że mamy do czynienia z funkcją f : nat -> (nat -> bool), czyli funkcją, która bierze liczbę naturalną i zwraca funkcję z liczb naturalnych w bool. Pamiętajmy jednak, że -> łączy w prawo i wobec tego typ f możemy zapisać też jako nat -> nat -> bool. Tak więc f jest funkcją, która bierze dwie liczby naturalne i zwraca element typu bool.
Dzięki temu zabiegowi możemy wyobrażać sobie f jako dwuwymiarową tabelkę, której wiersze i kolumny są indeksowane liczbami naturalnymi, a komórki w tabelce wypełnione są wartościami typu bool. Przyjmijmy, że pierwszy argument f to indeks wiersza, zaś drugi to indeks kolumny. W takim układzie f n m to wartość n-tej funkcji na argumencie m. Wobec tego twierdzenie możemy sparafrazować mówiąc, że każda funkcja nat -> bool znajduje się w którymś wierszu tabelki.
To tyle wyobraźni - możemy już udowodnić twierdzenie. Na początku oczywiście bierzemy dowolne f oraz zakładamy, że jest surjekcją, uprzednio odwijając definicję bycia surjekcją.
Teraz musimy jakoś wyciągnąć sprzeczność z hipotezy Hf, czyli, używając naszej tabelkowej parafrazy, znaleźć funkcję z nat w bool, która nie znajduje się w tabelce. A nie znajdować się w tabelce, panie Ferdku, to znaczy: różnić się od każdej funkcji z tabelki na jakimś argumencie.
Zamiast jednak szukać takiej funkcji po omacku, skonstruujmy ją z tego, co mamy pod ręką - czyli z naszej tabelki. Jak przerobić funkcje z tabelki na nową, której w nie ma w tabelce? Tu właśnie ujawnia się geniuszalność Cantora: użyjemy metody przekątniowej, czyli spojrzymy na przekątną naszej tabelki.
Definiujemy więc nową funkcję diagonal : nat -> bool następująco: dla argumentu n : nat bierzemy funkcję z n-tego wiersza w tabelce, patrzymy na n-tą kolumnę, czyli na wartość funkcji na argumencie n, i zwracamy negację tego, co tam znajdziemy. Czujesz sprzeczność?
Nasze założenie mówi, że diagonal znajduje się w którymś wierszu tabelki - niech ma on numer n. Wiemy jednak, że diagonal różni się od n-tej funkcji z tabelki na argumencie n, gdyż zdefiniowaliśmy diagonal n jako negację tej właśnie komórki w tabelce. Dostajemy stąd równość f n n = diagonal n = negb (f n n), co po analizie przypadków daje ostatecznie true = false lub false = true.
Voilà! Sprzeczność osiągnięta, a zatem początkowe założenie było błędne i nie istnieje żadna surjekcja z nat w nat -> bool.

Ćwiczenie

Udowodnij, że nie ma surjekcji z nat w nat -> nat. Czy jest surjekcja z nat -> bool w (nat -> bool) -> bool? A w nat -> bool -> bool?
Poznawszy twierdzenie Cantora, możemy powrócić do ścisłej pozytywności, czyż nie? Otóż nie, bo twierdzenie Cantora jest biedne. Żeby móc czerpać z niego niebotyczne profity, musimy najpierw uogólnić je na dowolne dwa typy A i B znajdując kryterium mówiące, kiedy nie istnieje surjekcja z A w A -> B.

Lemma Cantor' :
  forall
    {A B : Type}
    (f : A -> (A -> B))
    (modify : B -> B)
    (H : forall b : B, modify b <> b),
      ~ surjective f.
Proof.
  unfold surjective. intros A B f modify H Hf.
  pose (g := fun x : A => modify (f x x)).
  destruct (Hf g) as [x Hx].
  apply (f_equal (fun h => h x)) in Hx.
  unfold g in Hx. apply (H (f x x)).
  symmetry. assumption.
Qed.

Uogólnienie jest dość banalne. Najpierw zastępujemy nat i bool przez dowolne typy A i B. W oryginalnym twierdzeniu nie użyliśmy żadnej właściwości liczb naturalnych, więc nie musimy szukać żadnych kryteriów dla typu A. Nasza tabelka może równie dobrze być indeksowana elementami dowolnego typu - dalej jest to tabelka i dalej ma przekątną.
Twierdzenie było jednak zależne od pewnej właściwości bool, mianowicie funkcja diagonal była zdefiniowana jako negacja przekątnej. Było nam jednak potrzeba po prostu funkcji, która dla każdego elementu z przekątnej zwraca element bool od niego różny. Ponieważ bool ma dokładnie dwa elementy, to negacja jest jedyną taką funkcją.
Jednak w ogólnym przypadku dobra będzie dowolna B-endofunkcja bez punktów stałych. Ha! Nagły atak żargonu bezzębnych ryb, co? Zróbmy krótką przerwę, żeby zbadać sposób komunikacji tych czarodziejskich zwierząt pływających po uczelnianych korytarzach.
Endofunkcja to funkcja, która ma taką samą dziedzinę i przeciwdziedzinę. Można się zatem domyślać, że B-endofunkcja to funkcja o typie B -> B. Punkt stały zaś to takie x, że f x = x. Jest to więc dokładnie ta własność, której chcemy, żeby pożądana przez nas funkcja nie miała dla żadnego x. Jak widać, żargon bezzębnych ryb jest równie zwięzły jak niepenetrowalny dla zwykłych śmiertelników.
Podsumowując: w uogólnionym twierdzeniu Cantora nie wymagamy niczego od A, zaś od B wymagamy tylko, żeby istniała funkcja modify : B -> B, która spełnia forall b : B, modify b <> b. Dowód twierdzenia jest taki jak poprzednio, przy czym zastępujemy użycie negb przez modify.

Ćwiczenie

Znajdź jedyny słuszny typ B, dla którego nie istnieje żadna B-endofunkcja bez punktów stałych.
Podpowiedź: to zadanie jest naprawdę proste i naprawdę istnieje jedyny słuszny typ o tej właściwości.
Pytanie (bardzo trudne): czy da się udowodnić w Coqu, że istnieje dokładnie jeden taki typ? Jeżeli tak, to w jakim sensie typ ten jest unikalny i jakich aksjomatów potrzeba do przepchnięcia dowodu?


Twierdzenie Cantora jako młot na negatywność

Z Cantorem po naszej stronie możemy wreszcie kupić ruble... ekhem, możemy wreszcie zaprezentować ogólną metodę dowodzenia, że negatywne typy induktywne prowadzą do sprzeczności. Mimo szumnej nazwy ogólna metoda nie jest aż taka ogólna i czasem będziemy musieli się bonusowo napracować.

Module Example1.

Otworzymy sobie nowy moduł, żeby nie zaśmiecać globalnej przestrzeni nazw - wszystkie nasze złe typy będą się nazywały wut. Przy okazji, zdecydowanie powinieneś nabrać podejrzeń do tej nazwy - jeżeli coś w tej książce nazywa się wut, to musi to być złowrogie, podejrzane paskudztwo.

Unset Positivity Checking.
Inductive wut (A : Type) : Type :=
| C : (wut A -> A) -> wut A.
Set Positivity Checking.

Arguments C {A} _.

Podobnie jak poprzednio, żeby Coq pozwolił nam zdefiniować wut musimy na czas definicji wyłączyć sprawdzanie kryterium ścisłej pozytywności. Dlaczego bez wykonania tego zabiegu typ wut A jest nielegalny, a jego definicja zostałaby przez Coqa odrzucona? Poza wspomnianymi w poprzednim podrozdziale problemami filozoficznymi wynikającymi z nieterminacji, jest też drugi, bardziej namacalny powód: istnienie typu wut A jest sprzeczne z (uogólnionym) twierdzeniem Cantora.

Definition extract {A : Type} (w : wut A) : wut A -> A :=
match w with
| C f => f
end.

Powód tej sprzeczności jest dość prozaiczny: za pomocą konstruktora C możemy z dowolnej funkcji wut A -> A zrobić element wut A, a skoro tak, to dowolny element typu wut A możemy odpakować i wyjąć z niego funkcję o typie wut A -> A.

Lemma surjective_extract :
  forall A : Type, surjective (@extract A).
Proof.
  unfold surjective.
  intros A f.
  exists (C f).
  cbn. reflexivity.
Qed.

Skoro możemy włożyć dowolną funkcję, to znaczy, że dla każdej funkcji istnieje element, z którego możemy ją wyjąć, a zatem mamy do czynienia z surjekcją.

Lemma wut_illegal : False.
Proof.
  apply (Cantor' (@extract bool) negb).
    destruct b; inversion 1.
    apply surjective_extract.
Qed.

W połączeniu zaś z twierdzeniem Cantora surjekcja wut A -> (wut A -> A) prowadzi do sprzeczności - wystarczy za A wstawić bool.

End Example1.

Przykład może ci się jednak wydać niezadowalający - typ wut jest przecież dość nietypowy, bo ma tylko jeden konstruktor. A co, gdy konstruktorów jest więcej?
Początkowo miałem opisać kilka przypadków z większą liczbą konstruktorów, ale stwierdziłem, że jednak mi się nie chce. W ćwiczeniach zobaczymy, czy będziesz w stanie sam wykombinować, jak się z nimi uporać (wskazówka: jest to bardzo łatwe, wystarczy chcieć i nie być leniwym jak ja).

Ćwiczenie

Poniższe paskudztwo łamie prawo ścisłej pozytywności nie jednym, lecz aż dwoma swoimi konstruktorami. Udowodnij, że jego istnienie prowadzi do sprzeczności. Metoda jest podobna jak w naszym przykładzie, ale trzeba ją troszkę dostosować do zastanej sytuacji.

Module Exercise1.

Unset Positivity Checking.
Inductive wut : Type :=
| C0 : (wut -> bool) -> wut
| C1 : (wut -> nat) -> wut.
Set Positivity Checking.


Lemma wut_illegal : False.

End Exercise1.

Ćwiczenie

Poniższe paskudztwo ma jeden konstruktor negatywny, a drugi pozytywny, niczym typowa panienka z borderlajnem...
Polecenie jak w poprzednim ćwiczeniu.

Module Exercise2.

Unset Positivity Checking.
Inductive wut : Type :=
| C0 : (wut -> wut) -> wut
| C1 : nat -> wut.
Set Positivity Checking.


Lemma wut_illegal : False.

End Exercise2.

Ćwiczenie

Poniższy typ reprezentuje termy beztypowego rachunku lambda, gdzie V to typ reprezentujący zmienne. Co to za zwierzątko ten rachunek lambda to my się jeszcze przekonamy... chyba, oby.
Taki sposób reprezentowania rachunku lambda (i ogólnie składni języków programowania) nazywa się HOAS, co jest skrótem od ang. Higher Order Abstract Syntax. W wielu językach funkcyjnych jest to popularna technika, ale w Coqu, jak zaraz udowodnisz, jest ona nielegalna. Ława oburzonych jest rzecz jasna oburzona!

Module Exercise3.

Unset Positivity Checking.
Inductive Term (V : Type) : Type :=
| Var : V -> Term V
| Lam : (Term V -> Term V) -> Term V
| App : Term V -> Term V -> Term V.
Set Positivity Checking.

Arguments Var {V} _.
Arguments Lam {V} _.
Arguments App {V} _ _.


Lemma Term_illegal : False.

End Exercise3.

Ćwiczenie

Poniższe bydle jeszcze do niedawna wydawało mi się całkiem trudne i problematyczne, ale oczywiście jest bardzo proste. Uszy do góry i do dzieła!

Module Exercise4.

Unset Positivity Checking.
Inductive wut : Type :=
| C : (wut -> wut) -> wut.
Set Positivity Checking.


Lemma wut_illegal : False.

End Exercise4.

Poradnik rozpoznawania negatywnych typów induktywnych

Skoro już wiemy, że negatywne typy induktywne są wynalazkiem szatana, to czas podać proste kryterium na ich rozpoznawanie. Jeżeli jesteś sprytny, to pewnie sam zdążyłeś już zauważyć ogólną regułę. Jednak aby nie dyskryminować osób mało sprytnych, trzeba napisać ją wprost.
Kryterium jest banalne. Mając dany typ T musimy rzucić okiem na jego konstruktory, a konkretniej na ich argumenty. Argumenty nieindukcyjne (czyli o typach, w których nie występuje T) są zupełnie niegroźne i wobec tego powinniśmy je zignorować. Interesować nas będą wyłącznie argumenty indukcyjne, czyli takie, w których występuje typ T.
Najbardziej podstawowy typ argumentu indukcyjnego, czyli samo T, jest rzecz jasna niegroźny. To samo dotyczy argumentu indukcyjnego o typie nat -> T. Wprawdzie jest on typu funkcyjnego, co, jak się zaraz przekonamy, jest groźne, ale T występuje po prawej stronie strzałki, więc wszystko jest w porządku. W ogólności w porządku są też argumenty typu A -> T, gdzie A jest znanym typem niezależącym od T.
Niektóre typy argumentów indukcyjnych również są niegroźne, np. T * T, T + T, list T albo {t : T | P t}, ale ich użycie sprawia, że Coq nie potrafi wygenerować dla definiowanego typu odpowiedniej reguły indukcji, więc generuje jedynie regułę analizy przypadków. Te typy argumentów nie prowadzą do sprzeczności, ale powinniśmy ich unikać, bo są upierdliwe i każde takie wystąpienie argumentu indukcyjnego można łatwo zrefaktoryzować.
Argument typu T * T można zastąpić dwoma argumentami typu T i podobnie dla {t : T | P t}. Konstruktor z argumentem typu T + T możemy rozbić na dwa konstruktory (i powinniśmy, bo jest to bardziej czytelne). Konstruktor z wystąpieniem list T możemy przerobić na definicję przez indukcję wzajemną (ćwiczenie: sprawdź jak), ale lepiej chyba po prostu zaimplementować regułę indukcji ręcznie.

Inductive T0 : Type :=
| c0 : T0
| c1 : nat -> T0
| c2 : T0 -> T0
| c3 : nat * T0 -> T0
| c4 : T0 * T0 -> T0
| c5 : T0 + T0 -> T0
| c6 : list T0 -> T0
| c7 : (nat -> T0) -> T0.

Rodzaje nieszkodliwych typów argumentów widać na powyższym przykładzie. Konstruktory c0 i c1 są nieindukcyjne, więc są ok. Konstruktor c2 jest indukcyjny - jest jeden argument typu T0. Zauważ, że typem konstruktora c2 jest T0 -> T0, ale nie oznacza to, że T0 występuje po lewej stronie strzałki!
Jest tak, gdyż ostatnie wystąpienie T0 jest konkluzją konstruktora c2. Ważne są tylko wystąpienia po lewej stronie strzałki w argumentach (gdyby konstruktor c2 nie był legalny, to jedynymi legalnymi typami induktywnymi byłyby enumeracje).
Konstruktory c3, c4, c5 i c6 są induktywne i również w pełni legalne, ale są one powodem tego, że Coq nie generuje dla T0 reguły indukcji, a jedynie regułę analizy przypadków (choć nazywa się ona T0_ind). Konstruktor c7 również jest w pełni legalny.

Ćwiczenie

Zrefaktoryzuj powyższy upośledzony typ.


Problem pojawia się dopiero wtedy, gdy typ T występuje po lewej stronie strzałki, np. T -> bool, T -> T, (T -> T) -> T, lub gdy jest skwantyfikowany uniwersalnie, np. forall t : T, P t (typ T jest dziedziną kwantyfikacji) lub forall f : (forall t : T, P t), Q f (tym razem T jest dziedziną dziedziny kwantyfikacji).
W trzech poprzednich podrozdziałach mierzyliśmy się z sytuacjami, gdy typ T występował bezpośrednio na lewo od strzałki, ale oczywiście może on być dowolnie zagnieżdżony. Dla każdego wystąpienia T w argumentach możemy policzyć, na lewo od ilu strzałek się ono znajduje (czyli jak mocno zagnieżdżona jest dziedzina kwantyfikacji). Liczbę tę nazywać będziemy niedobrością. W zależności od niedobrości, wystąpienie nazywamy:
Jeżeli w definicji mamy wystąpienie negatywne, to typ możemy nazywać negatywnym typem induktywnym (choć oczywiście nie jest to legalny typ induktywny). Jeżeli nie ma wystąpień negatywnych, ale są wystąpienia pozytywne, to typ nazywamy pozytywnym typem induktywnym (lub nie-ściśle-pozytywnym typem induktywnym), choć oczywiście również nie jest to legalny typ induktywny. Jeżeli wszystkie wystąpienia są ściśle pozytywne, to mamy do czynienia po prostu z typem induktywnym. Parafrazując: każdy typ induktywny jest ściśle pozytywny.
Podobne nazewnictwo możemy sobie wprowadzić dla konstruktorów (konstruktory negatywne, pozytywne i ściśle pozytywne), ale nie ma sensu, bo za tydzień i tak zapomnisz o tych mało istotnych detalach. Ważne, żebyś zapamiętał najważniejsze, czyli ideę.

(*
Inductive T1 : Type :=
| T1_0 : T1 -> T1
| T1_1 : (T1 -> T1) -> T1
| T1_2 : ((T1 -> T1) -> T1) -> T1
| T1_3 : forall (t : T1) (P : T1 -> Prop), P t -> T1.
*)


W powyższym przykładzie wystąpienie T1 w pierwszym argumencie T1_0 jest ściśle pozytywne (na lewo od 0 strzałek). Pierwsze wystąpienie T1 w T1_1 jest negatywne (na lewo od 1 strzałki), a drugie ściśle pozytywne (na lewo od 0 strzałek). Pierwsze wystąpienie T1 w T1_2 jest pozytywne (na lewo od 2 strzałek), drugie negatywne (na lewo od 1 strzałki), trzecie zaś ściśle pozytywne (na lewo od 0 strzałek). Pierwsze wystąpienie T1 w T1_3 jest negatywne (dziedzina kwantyfikacji), drugie zaś pozytywne (na lewo od jednej strzałki, ale ta strzałka jest w typie, po którym kwantyfikujemy).
Konstruktor T1_0 jest ściśle pozytywny, zaś konstruktory T1_1, T1_2 oraz T1_3 są negatywne. Wobec tego typ T1 jest negatywnym typem induktywnym (czyli nie jest legalnym typem induktywnym i dlatego właśnie Coq odrzuca jego definicję).

Ćwiczenie


(*
Inductive T2 : Type :=
| T2_0 :
    forall f : (forall g : (forall t : T2, nat), Prop), T2
| T2_1 :
    (((((T2 -> T2) -> T2) -> T2) -> T2) -> T2) -> T2
| T2_2 :
  ((forall (n : nat) (P : T2 -> Prop),
    (forall t : T2, nat)) -> T2) -> T2 -> T2 -> T2.
*)


Policz niedobrość każdego wystąpienia T2 w powyższej definicji. Sklasyfikuj konstruktory jako negatywne, pozytywne lub ściśle pozytywne. Następnie sklasyfikuj sam typ jako negatywny, pozytywny lub ściśle pozytywny.

Ćwiczenie


(* Inductive T : Type := *)

Rozstrzygnij, czy następujące konstruktory spełniają kryterium ścisłej pozytywności. Jeżeli tak, narysuj wesołego jeża. Jeżeli nie, napisz zapętlającą się funkcję podobną do loop (zakładamy, że typ T ma tylko ten jeden konstruktor). Następnie sprawdź w Coqu, czy udzieliłeś poprawnej odpowiedzi.


Kilka bonusowych pułapek

Wiemy już, że niektóre typy argumentów indukcyjnych są ok (T, A -> T), inne problematyczne (T * T, list T), a jeszcze inne nielegalne (T -> T, forall t : T, ...). Uważny i żądny wiedzy czytelnik (daj boże, żeby tacy istnieli) zeche zapewne postawić sobie pytanie: które dokładnie typy argumentów indukcyjnych są ok, a które są wynalazkiem szatana?

Zabawy z parametrami

Najprościej będzie sprawę zbadać empirycznie, czyli na przykładzie. Żeby zaś przykład był reprezentatywny, niech parametrem definicji będzie dowolna funkcja F : Type -> Type.

Fail Inductive wut (F : Type -> Type) : Type :=
| wut_0 : F (wut F) -> wut F.
(* ===> The command has indeed failed with message:
        Non strictly positive occurrence of "wut" in
        "F (wut F) -> wut F". *)


Jak widać, jeżeli zaaplikujemy F do argumentu indukcyjnego, to Coq krzyczy, że to wystąpienie nie jest ściśle pozytywne. Dlaczego tak jest, skoro F nie jest ani strzałką, ani kwantyfikatorem uniwersalnym? Dlatego, że choć nie jest nimi, to może nimi zostać. Jeżeli zdefiniujemy sobie gdzieś na boku F := fun A : Type => A -> bool, to wtedy wut_0 F : (wut F -> bool) -> wut F, a z takim diabelstwem już się mierzyliśmy i wiemy, że nie wróży ono niczego dobrego.
Morał z tej historii jest dość banalny: gdy definiujemy typ induktywny T, jedynymi prawilnymi typami dla argumentu indukcyjnego są T oraz typy funkcji, które mają T jako konkluzję (A -> T, A -> B -> T etc.). Wszystkie inne albo rodzą problemy z automatyczną generacją reguł indukcji (T * T, list T), albo prowadzą do sprzeczności (T -> T, forall t : T, ...), albo mogą prowadzić do sprzeczności, jeżeli wstawi się za nie coś niedobrego (F T).

Ćwiczenie

Zdefiniuj rodzinę typów z powyższego przykładu (wyłączając sprawdzanie kryterium ścisłej pozytywności) i udowodnij, że jej istnienie prowadzi do sprzeczności.


Pułapki dla indukcji wzajemnej

To jeszcze nie koniec wrażeń na dziś - póki co omówiliśmy wszakże kryterium ścisłej pozytywności jedynie dla bardzo prostych typów induktywnych. Słowem nie zająknęliśmy się nawet na temat typów wzajemnie induktywnych czy indeksowanych typów induktywnych. Nie trudno będzie nam jednak uzupełnić naszą wiedzę, gdyż w przypadku oby tych mechanizmów kryterium ścisłej pozytywności wygląda podobnie jak w znanych nam już przypadkach (choć jest kilka kruczków, na które trzeba uważać). Spójrzmy na poniższy przykład:

Fail Inductive X : Type :=
| X0 : X
| X1 : (Y -> X) -> X

with Y : Type :=
| Y0 : Y
| Y1 : X -> Y.

(* ===> The command has indeed failed with message:
        Non strictly positive occurrence of "Y"
        in "(Y -> X) -> X". *)


Jak widać, powyższa definicja X i Y przez wzajemną indukcję jest nielegalna, gdyż jedyny argument konstruktora X1 ma typ Y -> X. Mogłoby wydawać się, że wszystko jest w porządku, wszakże X występuje tutaj na pozycji ściśle pozytywnej. Jednak ponieważ jest to definicja przez indukcję wzajemną, kryterium ścisłej pozytywności stosuje się nie tylko do wystąpień X, ale także do wystąpień Y - wszystkie wystąpienia X oraz Y muszą być ściśle pozytywne zarówno w konstruktorach typu X, jak i w konstruktorach typu Y.

Ćwiczenie

Zdefiniuj typy X i Y wyłączając positivity checker. Udowodnij za pomocą twierdzenia Cantora, że typy X i Y są nielegalne. Zdefiniuj też nieterminujące elementy loopx : X oraz loopy : Y i pobaw się nimi w trybie dowodzenia, żeby upewnić się, że faktyzcnie nie terminują. Pytanie bonusowe: czy do zdefiniowania loopx i loopy konieczna jest rekursja wzajemna?


Jeszcze więcej pułapek

To już prawie koniec naszej wędrówki przez świat nielegalnych typów "induktywnych". Dowiedzieliśmy się, że negatywne typy induktywne prowadzą do nieterminacji i nauczyliśmy się wykorzystywać twierdzenie Cantora do dowodzenia nielegalności takich typów.
Poznaliśmy też jednak klasyfikację typów wyglądających na induktywne (ściśle pozytywne, pozytywne, negatywne), a w szczególności pojęcie "niedobrości" indukcyjnego wystąpienia definiowanego typu w konstruktorze (upraszczając, na lewo od ilu strzałek znajduje się to wystąpienie).
Piszę "jednak", gdyż z jej powodu możemy czuć pewien niedosyt - wszystkie dotychczasowe przykłady były typami negatywnymi o niedobrości równej 1. Podczas naszej intelektualnej wędrówki zwiedziliśmy mniej miejscówek, niż moglibyśmy chcieć. W tym podrozdziale spróbujemy ten przykry niedosyt załatać, rozważając (nie ściśle) pozytywne typy induktywne. Zobaczymy formalny dowód na to, że nie są one legalne (lub, precyzyjniej pisząc, dowód na to, że conajmniej jeden z nich nie jest legalny). Zanim jednak to się stanie, zobaczmy, czy wypracowane przez nas techniki działają na negatywne typy induktywne o niedobrości innej niż 1.

Większa niedobrość


Module T3.

Unset Positivity Checking.
Inductive T3 : Type :=
| T3_0 : (((T3 -> bool) -> bool) -> bool) -> T3.
Set Positivity Checking.

Przyjrzyjmy się powyższej definicji. Występienie indukcyjne typu T3 ma współczynnik niedobrości równy 3, gdyż znajduje się na lewo od 3 strzałek. Prawe strony wszystkich z nich to bool. Zanim zobaczymy, jak pokazać nielegalność tego typu metodą Cantora, przypomnijmy sobie pewien kluczowy fakt dotyczący negacji i jego banalne uogólnienie.

Lemma triple_negation :
  forall P : Prop, ~ ~ ~ P -> ~ P.

Lemma triple_arrow :
  forall A B : Type, (((A -> B) -> B) -> B) -> (A -> B).

Fakt ten przypomina nam, że jeżeli chodzi o spamowanie negacją, to są w zasadzie tylko trzy sytuacje:
Jeżeli mamy do czynienia z większą liczbą negacji, to możemy zdejmować po dwie aż dojdziemy do któregoś z powyższych przypadków. Ponieważ negacja to tylko implikacja, której kodziedziną jest False, a nie korzystamy w dowodzie z żadnych specjalnych właściwości False, analogiczna właściwość zachodzi także dla dowolnego innego B : Type.

Definition extract (x : T3) : T3 -> bool :=
match x with
| T3_0 f => fun y : T3 => f (fun g => g y)
end.

Wobec powyższych rozważań definicja funkcji extract zupełnie nie powinna cię zaskakiwać (a jeżeli cię zaskakuje, to sprawdź jak wygląda term, który skonstruowałeś dowodząc triple_arrow - jeżeli zrobiłeś to dobrze, to powinien wyglądać podobnie do definicji extract). Szczerze pisząc, reszta dowodu również nie jest jakoś specjalnie wymagająca czy oświecająca.

Ćwiczenie

Dokończ dowód.

Lemma surjective_extract :
  surjective extract.

Lemma T3_illegal : False.

Ćwiczenie

Napisanie zapętlającej się funkcji loop : T3 -> bool też nie jest jakoś wybitnie trudne. Napisz ją i pobaw się nią w trybie dowodzenia, żeby przekonać się, że faktycznie nie terminuje (dla jakiegoś argumentu x : T3, który musisz sam wymyślić - to również nie jest specjalnie trudne).


End T3.

Morał z powyższych rozważań jest prosty: nasze techniki działają także na negatywne typy induktywne o niedobrości równej 3. Myślę, że jesteś całkiem skłonny uwierzyć też, że zadziałają na te o niedobrości równej 5, 7 i tak dalej.

Upierdliwe kodziedziny


Require Import FunctionalExtensionality.

To wszystko jest prawdą jednak tylko wtedy, gdy wszystkie typy po prawych stronach strzałek będą takie same. A co, gdy będą różne?

Module T4.

Unset Positivity Checking.
Inductive T4 : Type :=
| c0 : (((T4 -> bool) -> nat) -> Color) -> T4.
Set Positivity Checking.

Powyższy przykład jest podobny do poprzedniego, ale tym razem zamiast trzech wystąpień bool mamy bool, nat oraz Color (to ostatnie to typ, który zdefiniowaliśmy na samym początku tego rozdziału, gdy uczyliśmy się o enumeracjach).

Definition extract (x : T4) : T4 -> bool.
Proof.
  destruct x as [f].
  intros y.
  apply (
    fun c : Color =>
    match c with
    | R => true
    | _ => false
    end).
  apply f.
  intro g.
  apply (fun b : bool => if b then 0 else 1).
  apply g.
  exact y.
Defined.

Nasz modus operandi będzie taki jak poprzednio: spróbujemy wyjąć z elementu T4 funkcję typu T4 -> bool. W tym celu rozbijamy x i wprowadzamy y : T4 do kontekstu.
Tym razem nie możemy jednak bezpośrednio zaaplikować f, gdyż jej kodziedziną jest Color, a my musimy skonstruować coś typu bool. Możemy temu jednak zaradzić aplikując do celu skonstruowaną naprędce funkcję typu Color -> bool. Ta funkcja powinna być surjekcją (jeśli nie wierzysz, sprawdź, co się stanie, jeżeli zamienimy ją na funckję stałą albo inną nie-surjekcję).
Możemy już zaaplikować f i wprowadzić g do kontekstu. Chcielibyśmy teraz zaaplikować g, ale nie możemy, bo typy się nie zgadzają - g zwraca bool, a my musimy skonstruować liczbę naturalną. Robimy tutaj to samo co poprzednio - aplikujemy do celu jakąś funkcję bool -> nat. Tym razem nie musi ona być surjekcją (nie jest to nawet możliwe, gdyż nie ma surjekcji z bool w nat). Dzięki temu możemy zaaplikować g i zakończyć, używając y.
Żeby pokazać, że extract jest surjekcją, będziemy potrzebować aksjomatu ekstensjonalności dla funkcji (ang. functional extensionality axiom, w skrócie funext). Głosi on, że dwie funkcje f, g : A -> B są równe, jeżeli uda nam się pokazać, że dają równe wyniki dla każdego argumentu (czyli forall x : A, f x = g x).
Importując moduł FunctionalExtensionality zakładamy prawdziwość tego aksjomatu oraz uzyskujemy dostęp do taktyki extensionality, która ułatwia dowody wymagające użycia ekstensjonalności.

Lemma surjective_extract :
  surjective extract.
Proof.
  unfold surjective, extract.
  intro f.
  exists (c0 (
    fun g : (T4 -> bool) -> nat =>
    match g f with
   | 0 => R
   | _ => G
    end)).
  extensionality y.
  destruct (f y); reflexivity.
Qed.

Dowód jest prawie taki jak zawsze: odwijamy definicję surjektywności i wprowadzamy hipotezy do kontekstu, a następnie odwijamy definicję extract i rozbijamy ją dla czytelności na właściwą funkcję extract oraz równanie eq.
Następnie musimy znaleźć a : T4, które extract mapuje na f. Zaczynamy od c0, bo jest to jedyny konstruktor T4. Bierze on jako argument funkcję typu ((T4 -> bool) -> nat) -> Color. Żeby ją wyprodukować, bierzemy na wejściu funkcję g : (T4 -> bool) -> nat i musimy zrobić coś typu Color.
Nie może to być jednak byle co - musimy użyć f, a jedynym sensownym sposobem, żeby to zrobić, jest zaaplikować g do f. Musimy zadbać też o to, żeby odwrócić funkcje konwertujące Color -> bool oraz bool -> nat, których użyliśmy w definicji extract. Pierwsza z nich konwertowała R (czyli kolor czerwony) na true, a inne kolory na false, zaś druga konwertowała true na 0, a false na 1. Wobec tego dopasowując g f : nat musimy przekonwertować 0 na R, zaś 1 na coś innego niż R, np. na G (czyli kolor zielony).
Znalazłszy odpowiedni argument, możemy przepisać równanie definiujące extract. To już prawie koniec, ale próba użycia taktyki reflexivity w tym momencie skończyłaby się porażką. Na ratunek przychodzi nam aksjomat ekstensjonalności, którego używamy pisząc extensionality y. Dzięki temu pozostaje nam pokazać jedynie, że f y jest równe tej drugie funkcji dla argumentu y. W tym celu rozbijamy f y, a oba wyrażenia okazują się być konwertowalne.

Lemma T4_illegal : False.
Proof.
  apply (Cantor' extract negb).
    destruct b; inversion 1.
    apply surjective_extract.
Qed.

Skoro mamy surjekcję z T4 w T4 -> bool, katastrofy nie da się uniknąć.
Moglibyśmy się też zastanowić nad napisaniem zapętlającej się funkcji loop, ale coś czuję, że ty coś czujesz, że byłoby to babranie się w niepotrzebnym problemie. Wobec tego (oczywiście o ile dotychczas się nie skapnąłeś) poczuj się oświecony!

Definition loop (x : T4) : bool := extract x x.

Ha! Tak tak, loop nie jest niczym innym niż lekko rozmnożoną wersją extract.

Lemma extract_c0 :
  forall f : ((T4 -> bool) -> nat) -> Color,
    extract (c0 f) =
      fun y =>
        match f (fun g => if g y then 0 else 1) with
        | R => true
        | _ => false
        end.
Proof.
  reflexivity.
Qed.

Lemma loop_nontermination :
  true = loop (c0 (
    fun g : (T4 -> bool) -> nat =>
    match g loop with
   | 0 => R
   | _ => G
    end)).
Proof.
  intros.
  unfold loop.
  rewrite 5!extract_c0.
Abort.

A skoro loop to tylko inne extract, to nie powinno cię też wcale a wcale zdziwić, że najbardziej oczywisty argument, dla którego loop się zapętla, jest żywcem wzięty z dowodu surjective_extract (choć oczywiście musimy zastąpić f przez loop).
Oczywiście niemożliwe jest, żeby formalnie udowodnić w Coqu, że coś się zapętla. Powyższy lemat ma być jedynie demonstracją - ręczne rozpisanie tego przykładu byłoby zbyt karkołomne. Jak widać z dowodu, przepisywanie równania definiującego extract tworzy wesołą piramidkę zrobioną z matchów i ifów. Jeżeli chcesz poczuć pełnię zapętlenia, wypbróuj taktykę rewrite !eq - zapętli się ona, gdyż równanie eq można przepisywać w nieskończoność.

End T4.

Mogłoby się wydawać, że teraz to już na pewno nasze metody działają na wszystkie możliwe negatywne typy induktywne. Cytując Tadeusza Sznuka: "Nic bardziej mylnego!".

Module T5.

Unset Positivity Checking.
Inductive T5 : Type :=
| c0 : (((T5 -> nat) -> bool) -> Color) -> T5.
Set Positivity Checking.

Rzućmy okiem na powyższy typ. Wygląda podobnie do poprzedniego, ale jest nieco inny - typy nat i bool zamieniły się miejscami. Jakie rodzi to konsekwencje? Sprawdźmy.

Definition extract : T5 -> T5 -> nat.
Proof.
  intros [f] y.
  apply (
    fun c : Color =>
    match c with
    | R => 0
    | G => 1
    | B => 2
    end).
  apply f. intro g.
  apply isZero. exact (g y).
Defined.

Definicja extract jest podobna jak poprzednio, ale tym razem konwertujemy Color na nat za pomocą funkcji, która nie jest surjekcją.

Lemma surjective_extract :
  surjective extract.
Proof.
  unfold surjective, extract.
  intro f.
  exists (c0 (
    fun g : (T5 -> nat) -> bool =>
    match g f with
    | true => R
    | false => B
    end)).
  extensionality y.
  destruct (f y); cbn.
    reflexivity.
Abort.

Dowód również przebiega podobnie jak poprzednio. Załamuje się on dopiero, gdy na samym końcu rozbijamy wyrażenie f y i upraszczamy używając cbn. W pierwszym podcelu 0 = 0 jeszcze jakoś udaje się nam udowodnić, ale w drugim naszym oczom ukazuje się cel 2 = S n.
Problem polega na tym, że f y może być dowolną liczbą naturalną, ale zastosowana przez nas funkcja konwertująca Color -> nat może zwracać jedynie 0, 1 lub 2. Teraz widzimy jak na dłoni, skąd wziął się wymóg, by funkcja konwertująca była surjekcją.

Definition loop (x : T5) : nat := extract x x.

Lemma extract_eq :
  forall (f : ((T5 -> nat) -> bool) -> Color) (y : T5),
    extract (c0 f) y =
      match f (fun g : T5 -> nat => isZero (g y)) with
      | R => 0
      | G => 1
      | B => 2
      end.
Proof.
  reflexivity.
Qed.

Lemma loop_nontermination :
  42 = loop (c0 (
    fun g : (T5 -> nat) -> bool =>
    match g loop with
    | true => R
    | false => G
    end)).
Proof.
  unfold loop.
  rewrite extract_eq.
  rewrite 2!extract_eq.
Abort.

Co ciekawe, mimo że nie jesteśmy w stanie pokazać surjektywności extract, to wciąż możemy użyć tej funkcji do zdefiniowania zapętlającej się funkcji loop, zupełnie jak w poprzednim przykładzie.
Niesmak jednak pozostaje, gdyż szczytem naszych ambicji nie powinno być ograniczanie się do zdefiniowania loop, lecz do formalnego udowodnienia nielegalności T5. Czy wszystko stracone? Czy umrzemy? Tu dramatyczna pauza.
Nie (w sensie że nie stracone, chociaż oczywiście umrzemy jak każdy).
Okazuje się, że jest pewien trikowy sposób na rozwiązanie tego problemu, a mianowicie: zamiast próbować wyjąć z T5 funkcję T5 -> nat, wyjmiemy stamtąd po prostu funckję T5 -> bool i to mimo tego, że jej tam nie ma!

Definition extract' : T5 -> (T5 -> bool).
Proof.
  intros [f] y.
  apply (
    fun c : Color =>
    match c with
    | R => true
    | _ => false
    end).
  apply f. intro g.
  apply isZero. exact (g y).
Defined.

W kluczowych momentach najpierw konwertujemy Color na bool tak jak w jednym z poprzednich przykładów, a potem konwertujemy nat na bool za pomocą funkcji isZero.

Lemma surjective_extract' :
  surjective extract'.
Proof.
  unfold surjective, extract'.
  intro f.
  exists (c0 (
    fun g : (T5 -> nat) -> bool =>
      if g (fun t : T5 => if f t then 0 else 1) then R else G)).
  extensionality y.
  destruct (f y); cbn; reflexivity.
Qed.

Ponieważ obydwie nasze funkcję konwertujące były surjekcjami, możemy je teraz odwrócić i wykazać ponad wszelką wątpliwość, że extract' faktycznie jest surjekcją.

Lemma T5_illegal : False.
Proof.
  apply (Cantor' extract' negb).
    destruct b; inversion 1.
    apply surjective_extract'.
Qed.

Spróbujmy podsumować, co tak naprawdę stało się w tym przykładzie.
Tym razem, mimo że do T5 możemy włożyć dowolną funkcję T5 -> nat, to nie możemy jej potem wyjąć, uzyskując surjekcję, gdyż zawadzają nam w tym typy po prawych stronach strzałek (bool i Color), które mają za mało elementów, żeby móc surjektywnie przekonwertować je na typ nat.
Jednak jeżeli mamy wszystkie możliwe funkcje typu T5 -> nat, to możemy przerobić je (w locie, podczas "wyciągania") na wszystkie możliwe funkcje typu T5 -> bool, składając je z odpowiednią surjekcją (np. isZero). Ponieważ typ bool i Color jesteśmy w stanie surjektywnie przekonwertować na bool, reszta procesu działa podobnie jak w poprzednich przykładach.

Definition loop' (x : T5) : bool := extract' x x.

Lemma extract'_eq :
  forall (f : ((T5 -> nat) -> bool) -> Color) (y : T5),
    extract' (c0 f) y =
      match f (fun g : T5 -> nat => isZero (g y)) with
      | R => true
      | _ => false
      end.
Proof.
  reflexivity.
Qed.

Lemma loop_nontermination :
  true = loop' (c0 (
    fun g : (T5 -> nat) -> bool =>
    match g (fun t : T5 => if loop' t then 0 else 1) with
    | true => R
    | false => G
    end)).
Proof.
  unfold loop'.
  rewrite 3!extract'_eq.
Abort.

Takie trikowe extract' wciąż pozwala nam bez większych przeszkód zdefiniować zapętlającą się funkcję loop'. Osiągnęliśmy więc pełen sukces.
W ogólności nasz trik możnaby sformułować tak: jeżeli mamy konstruktor negatywny typu T, to możemy wyjąć z niego funkcję T -> A, gdzie A jest najmniejszym z typów występujących po prawych stronach strzałek.

End T5.

No, teraz to już na pewno mamy obcykane wszystkie przypadki, prawda? Tadeuszu Sznuku przybywaj: "Otóż nie tym razem!".

Module T6.

Unset Positivity Checking.
Inductive T6 : Type :=
| c0 : (((T6 -> unit) -> bool) -> Color) -> T6.
Set Positivity Checking.

Kolejnym upierdliwym przypadkiem, burzącym nawet nasz ostateczny trik, jest sytuacja, w której po prawej stronie strzałki wystąpi typ unit. Oczywiście zgodnie z trikiem możemy z T6 wyciągnąć surjekcję T6 -> unit, ale jest ona oczywiście bezużyteczna, bo taką samą możemy zrobić za darmo, stale zwracając po prostu tt. Surjekcja z T6 w T6 -> unit nie wystarczy rzecz jasna, żeby odpalić twierdzenie Cantora.
Tym razem jednak nie powinniśmy spodziewać się, że upierdliwość tę będzie dało się jakoś obejść. Typ T6 -> unit jest jednoelementowy (jedynym elementem jest fun _ => tt) podobnie jak unit. Bardziej poetycko możemy powiedzieć, że T6 -> unit i unit są izomorficzne, czyli możemy bez żadnych strat konwertować elementy jednego z tych typów na elementy drugiego i z powrotem.
Skoro tak, to typ konstruktora c0, czyli (((T6 -> unit) -> bool) -> Color) -> T6), możemy równie dobrze zapisać jako ((unit -> bool) -> Color) -> T6). Zauważmy teraz, że unit -> bool jest izomorficzne z bool, gdyż ma tylko dwa elementy, a mianowicie fun _ => true oraz fun _ => false. Tak więc typ c0 możemy jeszcze prościej zapisać jako (bool -> Color) -> T6, a to oznacza, że typ T6 jest jedynie owijką na funkcje typu bool -> Color. Twierdzenie Cantora nie pozwala tutaj uzyskać sprzeczności.
Czy zatem typy takie jak T6 sa legalne? Syntaktycznie nie - Coq odrzuca je podobnie jak wszystkie inne negatywne typy induktywne. Semantycznie również nie - o ile nie możemy tutaj uzyskać jawnej sprzeczności, to nasze rozważania o nieterminacji wciąż są w mocy.
Przypomnij sobie poprzedni przykład i nieudaną próbę wyłuskania z T5 surjekcji T5 -> nat. Udało nam się zaimplementować funkcję extract, której surjektywności nie potrafiliśmy pokazać, ale pomimo tego bez problemu udało nam się użyć jej do napisania funkcji loop. W obecnym przykładzie jest podobnie i nieterminacja to najlepsze, na co możemy liczyć.

Ćwiczenie

Zdefiniuj funkcję extract, a następnie użyj jej do zdefiniowania funkcji loop. Zademonstruj w sposób podobny jak poprzednio, że loop się zapętla. Wskazówka: wszystko działa tak samo jak w poprzednim przykładzie.


End T6.

No, teraz to już na pewno wiemy wszystko...

Ćwiczenie

Otóż nie do końca. Ostatnim hamulcowym, groźniejszym nawet niż unit, jest wystąpienie po prawej stronie strzałki typu (czy raczej zdania) False. W tym przypadku nie tylko nie pomaga nam Cantor, ale nie pomaga też nieterminacja, gdyż najzwyczajniej w świecie nie da się zdefiniować żadnej funkcji.
Jako, że za cholerę nie wiem, co z tym fantem zrobić, zostawiam go tobie jako ćwiczenie: wymyśl metodę pokazywania nielegalności negatywnych typów induktywnych, w których po prawej stronie strzałki jest co najmniej jedno wystąpienie False.

Module T7.

Unset Positivity Checking.
Inductive T7 : Type :=
| c0 : (((T7 -> bool) -> False) -> Color) -> T7.
Set Positivity Checking.


End T7.

Pozytywne typy induktywne

Na koniec rozprawimy się z pozytywnymi typami "induktywnymi" (ale tylko do pewnego stopnia; tak po prawdzie, to raczej one rozprawią się z nami).

Fail Inductive Pos : Type :=
| Pos0 : ((Pos -> bool) -> bool) -> Pos.
(* ===> The command has indeed failed with message:
        Non strictly positive occurrence of "Pos" in
        "((Pos -> bool) -> bool) -> Pos". *)


Coq odrzuca powyższą definicję typu Pos, gdyż pierwsze wystąpienie Pos w typie konstruktora Pos0 nie jest ściśle pozytywne. I faktycznie - gdy policzymy niedobrość tego wystąpienia zgodnie z naszym wzorem, to wyjdzie, że wynosi ona 2, gdyż Pos występuje na lewo od dwóch strzałek (pamiętaj, że najbardziej zewnętrzna strzałka, czyli ta, na prawo od której też jest Pos, nie liczy się - wzór dotyczy tylko argumentów konstruktora, a nie całego konstruktora).

Unset Positivity Checking.
Inductive Pos : Type :=
| Pos0 : ((Pos -> bool) -> bool) -> Pos.
Set Positivity Checking.

Spróbujmy zawalczyć z typem Pos naszą metodą opartą o twierdzenie Cantora.

Definition extract (p : Pos) : Pos -> bool.
Proof.
  destruct p as [f]. intro y.
  apply f. intro z.
  apply f. intro w.
  (* ad infinitum *)
Abort.

Mogłoby się wydawać, że wyciągnięcie z Pos funkcji Pos -> bool nie może być trudniejsze, niż zabranie dziecku cukierka. Niestety jednak nie jest tak, gdyż w Pos tak naprawdę nie ma żadnej takiej funkcji - jest funkcja (Pos -> bool) -> bool, a to już zupełnie coś innego.
Żeby lepiej zrozumieć tę materię, musimy metaforycznie zinterpretować znany nam już współczynnik niedobrości i wynikający z niego podział na wystąpienia ściśle pozytywne, pozytywne i negatywne. Dzięki tej interpretacji dowiemy się też, dlaczego nieparzysta niedobrość jest negatywna, a niezerowa parzysta jest pozytywna.
Najprościej jest zinterpretować wystąpienia ściśle pozytywne, gdyż mieliśmy już z nimi sporo do czynienia. Weźmy konstruktor cons : A -> list A -> list A. Jest tutaj jedno ściśle pozytywne wystąpienie typu list A, które możemy interpretować tak: gdy używamy dopasowania do wzorca i dopasuje się cons h t, to "mamy" element t typu list A. Ot i cała filozofia.
Załóżmy teraz na chwilę, że Coq akceptuje negatywne i pozytywne typy induktywne. Co by było, gdybyśmy dopasowali konstruktor postaci c : (T -> bool) -> T? Tym razem nie mamy elementu typu T, lecz funkcję f : T -> bool. Parafrazując: musimy "dać" funkcji f element typu T, żeby dostać bool.
A co by było, gdybyśmy dopasowali konstruktor postaci c : ((T -> bool) -> bool) -> T? Tym razem również nie mamy żadnego elementu typu T, lecz funkcję f : ((T -> bool) -> bool). Parafrazując: musimy dać funkcji f jakąś funkcję typu T -> bool, żeby dostać bool. Ale gdy konstruujemy funkcję T -> bool, to na wejściu dostajemy T. Tak więc początkowo nie mamy żadnego T, ale gdy o nie poprosimy, to możemy je dostać. Ba! Jak pokazuje przykład, możemy dostać bardzo dużo T.
Taka właśnie jest różnica między ścisłą pozytywnością (mamy coś), negatywnością (musimy coś dać) i pozytywnością (możemy coś dostać, i to nawet w dużej liczbie sztuk). Zauważmy, że jedynie w przypadku negatywnym możemy wyjąć z T funkcję T -> coś (chyba, że zawadza nam unit lub False), bo to jedyny przypadek, gdy żądają od nas T (a skoro żądają T, to muszą mieć funkcję, która coś z tym T zrobi). W przypadku pozytywnym nie ma żadnej takiej funkcji - to my dostajemy T i musimy coś z niego wyprodukować, więc to my jesteśmy tą funkcją!
Ufff... mam nadzieję, że powyższa bajeczka jest sformułowana zrozumiale, bo lepszego wytłumaczenia nie udało mi się wymyślić.
Moglibyśmy w tym miejscu zastanowić się, czy nie uda nam się pokazać sprzeczności choć na metapoziomie, poprzez napisanie nieterminującej funkcji loop. Szczerze pisząc, to niezbyt w to wierzę. Przypomnij sobie, że okazało się, że funkcja loop jest bardzo ściśle powiązana z funkcją extract, zaś esencja nieterminacji polegała na przekazaniu do loop jako argument czegoś, co zawierało loop jako podterm (jeżeli nie zauważyłeś, to wszystkie nasze nieterminujące funkcje udało nam się zdefiniować bez użycia rekursji!). To daje nam jako taką podstawę by wierzyć, że nawet nieterminacja nie jest w tym przypadku osiągalna.


W tym momencie należy sobie zadać zasadnicze pytanie: dlaczego w ogóle pozytywne typy induktywne są nielegalne? Przecież odróżnienie wystąpienia pozytywnego od negatywnego nie jest czymś trudnym, więc Coq nie może ich od tak po prostu nie rozróżniać - musi mieć jakiś powód!
I faktycznie, powód jest. Nie ma on jednak wiele wspólnego z mechanizmem (pozytywnych) typów induktywnych samym w sobie, a z impredykatywnością sortu Prop. Trudne słowo, co? Nie pamiętam, czy już to wyjaśniałem, więc wyjaśnię jeszcze raz.
Impredykatywność (lub też impredykatywizm) to pewna forma autoreferencji, która czasem jest nieszkodliwa, a czasem bardzo mordercza. Przyjrzyjmy się następującej definicji: "wujek Janusz to najbardziej wąsata osoba w tym pokoju". Definicja ta jest impredykatywna, gdyż definiuje ona wujka Janusza poprzez wyróżnienie go z pewnej kolekcji osób, ale definicja tej kolekcji osób musi odwoływać się do wujka Janusza ("w pokoju są wujek Janusz, ciocia Grażynka, Sebastianek i Karynka"). W Coqu impredykatywny jest sort Prop, co ilustruje przykład:

Definition X : Prop := forall P : Prop, P.

Definicja zdania X jest impredykatywna, gdyż kwantyfikujemy w niej po wszystkich zdaniach (forall P : Prop), a zatem kwantyfikujemy także po zdaniu X, które właśnie definiujemy.
Impredykatywność sortu Prop jest niegroźna (no chyba, że pragniemy pozytywnych typów induktywnych, to wtedy jest), ale impredykatywność dla Type byłaby zabójcza, co zresztą powinien nam był uświadomić paradoks Russella.
Dobra, koniec gadania. Poniższy przykład pośrednio pochodzi z sekcji 3.1 pracy "Inductively defined types", której autorami są Thierry Coquand oraz Christine Paulin-Mohring, zaś bezpośrednio jest przeróbką kodu wziętego z vilhelms.github.io/posts/why-must-inductive-types-be-strictly-positive

Unset Positivity Checking.
Inductive Pos' : Type :=
| Pos'0 : ((Pos' -> Prop) -> Prop) -> Pos'.
Set Positivity Checking.

Jak widać, podejrzanym typem jest Pos', bliźniaczo podobne do Pos, ale zamiast bool występuje tutaj Prop.
Tym razem jednak dowód sprzeczności będzie przebiegał nieco inaczej niż dotychczas. Poprzednio nasze plany sprowadzały się do tego, żeby wyjąć z nielegalnego typu induktywnego T jakąś funkcję T -> A, co daje nam surjekcję T -> (T -> A), a to jest sprzeczne z twierdzeniem Cantora.
Intuicja zaś stojąca za tym twierdzeniem (przynajmniej dla A = bool) była taka, że funkcje f : T -> bool są zbiorami elementów typu A i jest ich przez to "więcej" niż elementów T, czyli T -> bool jest większa niż T. Surjekcją z T w T -> bool oznacza jednak, że to T jest większe niż T -> bool, co prowadzi do sprzeczności.
Tym razem nie jesteśmy w stanie zrobić żadnej surjekcji, ale możemy za to zrobić injekcję. Intuicyjnie injekcja z A w B oznacza, że każdemu elementowi A można przypisać unikalny element B. Jeszcze intuicyjniej: typ A jest w jakimś sensie "mniejszy" niż typ B, czyli jeszcze inaczej pisząc, typ A można "włożyć" lub "wstrzyknąć" (i stąd nazwa) do typu B.
Nasz plan polega więc na tym, żeby zdefiniować injekcję (Pos' -> Prop) -> Pos', co powinno jakoś prowadzić do sprzeczności: istnienie takiej injekcji oznacza, że Pos' -> Prop jest "mniejszy" niż Pos', ale z twierdzenia Cantora powinniśmy intuicyjnie czuć, że Pos' -> Prop jest większe niż Pos'.

Definition f (P : Pos' -> Prop) : Pos' :=
  Pos'0 (fun Q : Pos' -> Prop => P = Q).

Lemma injective_f :
  forall P Q : Pos' -> Prop,
    f P = f Q -> P = Q.
Proof.
  unfold f.
  inversion 1.
  apply (f_equal (fun f => f Q)) in H1.
  rewrite H1.
  reflexivity.
Qed.

Definicja naszej injekcji jest dość prosta. Żeby przerobić P : Pos' -> Prop na Pos', używamy konstruktora Pos'0, a jego argumentem jest po prostu funkcja, która porównuje swój argument do P używając relacji równości.

Definition wut (x : Pos') : Prop :=
  exists P : Pos' -> Prop, f P = x /\ ~ P x.

Tutaj następują czary, które używają impredykatywności.
Definiujemy predykat wut : Pos' -> Prop, który głosi, że funkcja f jest surjekcją (czyli exists P, f P = x), która ma dodatkowo tę właściwość, że predykat P, któremu f przyporządkowuje x, nie jest spełniony przez x (czyli ~ P x).
Parafrazując: wut to coś w stylu zbioru wszystkich x : Pos', które nie należą same same do siebie. Paradoks Russella jak malowany! Wielka zła autoreferencja czai się tuż za rogiem - ciekawe co by się stało, gdybyśmy rozważyli zdanie wut (f wut)...

Lemma paradox : wut (f wut) <-> ~ wut (f wut).
Proof.
  split.
    destruct 1 as (P & H1 & H2). intro H.
      apply injective_f in H1. subst. contradiction.
    intro H. red. exists wut. split.
      reflexivity.
      assumption.
Qed.

Ano, wszystko wybucha. Z lewa na prawo rozbijamy dowód wut (f wut) i dostajemy predykat P. Wiemy, że f P = f wut, ale f jest injekcją, więc P = wut. To jednak kończy się sprzecznością, bo z jednej strony wut (f wut), ale z drugiej strony ~ P (f wut), czyli ~ wut (f wut).
Z prawa na lewo jest łatwiej. Mamy ~ wut (f wut) i musimy udowodnić wut (f wut). Wystarczy, że istnieje pewien predykat P, który spełnia f P = f wut i ~ P (f wut). Na P wybieramy oczywiście wut. Równość f wut = f wut zachodzi trywialnie, zaś ~ wut (f wut) zachodzi na mocy założenia.

Lemma Pos'_illegal : False.
Proof.
  pose paradox. firstorder.
Qed.

No i bum. Jak widać, pozytywne typy induktywne prowadzą do sprzeczności, ale nie ma to z nimi wiele wspólnego, za to ma wiele wspólnego z sortem Prop i jego impredykatywnością.

Podsumowanie (TODO)

W tym rozdziale poznaliśmy kryterium ścisłej pozytywności, które obowiązuje wszystkie definicje typów induktywnych. Dowiedzieliśmy się, że negatywne typy induktywne prowadzą do nieterminacji, która jest złem wcielonym. Poznaliśmy pojęcie surjekcji oraz twierdzenie Cantora, które również zabrania negatywnym typom induktywnym istnienia.
Ostatecznie dowiedzieliśmy się, że pozytywne typy induktywne także są nielegalne, choć jesteśmy wobec nich raczej bezsilni, no chyba że chodzi o impredykatywny (tego słowa też się nauczyliśmy) sort Prop.