13/11/2020
W świecie informatyki i matematyki istnieją koncepcje, które na pierwszy rzut oka wydają się całkowicie odrębne, a jednak po bliższym przyjrzeniu okazują się być ze sobą głęboko powiązane. Jednym z najbardziej zdumiewających przykładów takiego powiązania jest izomorfizm Curry'ego-Howarda (w skrócie CH). Mówi nam on, że aby dowieść dowolnego twierdzenia matematycznego, wystarczy skonstruować pewien typ danych, który odzwierciedla naturę tego twierdzenia, a następnie znaleźć wartość, która ma ten typ. Brzmi to niezwykle dziwnie: co typy mają wspólnego z twierdzeniami? Jak zobaczymy, te dwie koncepcje są ze sobą bardzo ściśle związane, otwierając nowe perspektywy na zrozumienie zarówno programowania, jak i logiki matematycznej. Na początek pomijamy istnienie wyrażeń takich jak error czy undefined, których semantyka denotacyjna to ⊥. Odgrywają one niezwykle ważną rolę, ale rozważymy je osobno w odpowiednim czasie. Ignorujemy również funkcje, które omijają system typów, takie jak unsafeCoerce#.

Możemy budować niezwykle skomplikowane typy, korzystając z funkcji wyższego rzędu, dostępnych na przykład w języku Haskell. Możemy zadać sobie pytanie: biorąc pod uwagę dowolny typ, pod jakimi warunkami istnieje wartość tego typu (mówimy wtedy, że typ jest zamieszkany)? Pierwszą myślą mogłoby być: „zawsze”, ale szybko to się załamuje pod wpływem przykładów. Na przykład, nie ma funkcji typu a -> b, ponieważ nie mamy sposobu, aby zamienić coś typu a w coś zupełnie innego typu b (chyba że z góry wiemy, jakimi typami są a i b, w takim przypadku mówimy o funkcji monomorficznej, takiej jak ord :: Char -> Int).
Co niezwykłe, okazuje się, że typ jest zamieszkany tylko wtedy, gdy odpowiada prawdziwemu twierdzeniu w logice matematycznej. Ale jaka jest natura tej korespondencji? Co oznacza typ taki jak a -> b w kontekście logiki?
Szybki kurs logiki formalnej
Zanim zaczniemy badać związek logiki formalnej z teorią typów, potrzebujemy pewnych podstaw. Jest to bardzo krótkie wprowadzenie; w celu szerszego ugruntowania zalecamy zapoznanie się z podręcznikiem wprowadzającym do tego przedmiotu.
W języku potocznym często używamy zdań typu „Jeśli… to…”. Na przykład: „Jeśli dziś będzie ładna pogoda, to pójdziemy na spacer do miasta”. Tego rodzaju stwierdzenia pojawiają się również w matematyce; możemy powiedzieć na przykład: „Jeśli x jest dodatnie, to ma (rzeczywisty) pierwiastek kwadratowy”. Logika formalna to sposób reprezentowania stwierdzeń, które przybliżają znaczenia angielskie za pomocą logiki Boole’a, na której możemy wykonywać obliczenia. Używamy symbolu A→B (czytanego jako „A implikuje B”), aby wskazać, że B jest prawdziwe, ilekroć A jest prawdziwe. Na przykład, nasze wcześniejsze stwierdzenie mogłoby zostać przeformułowane jako „x jest dodatnie → x ma rzeczywisty pierwiastek kwadratowy”, co oznacza, że dodatniość liczby implikuje istnienie pożądanego rodzaju pierwiastka. Często będziemy używać liter do oznaczania całych stwierdzeń; na przykład, jeśli W oznacza stwierdzenie „pogoda jest ładna”, a T oznacza stwierdzenie „pójdziemy na spacer do miasta”, to moglibyśmy zapisać W → T.
Nasza definicja P→Q ma pewne wady. Jeśli Q jest stwierdzeniem, które jest zawsze prawdziwe, niezależnie od okoliczności – na przykład „słońce jest gorące” – to nie ma znaczenia, czym jest P. P mogłoby być nawet fałszywym stwierdzeniem, Q nadal byłoby prawdziwe, gdyby P było prawdziwe, więc implikacja P → Q nie jest uważana za błędną. P → Q jest definiowane jako prawdziwe, ilekroć P jest fałszywe lub Q jest prawdziwe. Zatem → tak naprawdę nie reprezentuje żadnego związku przyczynowo-skutkowego; stwierdzenia takie jak „niebo jest różowe → słońce jest gorące” są definiowane jako prawdziwe. Istnieją inne algebry logiczne poza logiką Boole’a, które próbują naprawić te „problemy”, i możemy je również konstruować w Haskellu.
Inne rzeczy, które często pojawiają się zarówno w języku potocznym, jak i w matematyce, to tak zwane koniunkcje i dysjunkcje. Koniunkcje reprezentują stwierdzenia zawierające „i”, dysjunkcje – stwierdzenia zawierające „lub”. Stwierdzenie „Kupię ten magazyn, jeśli będzie dostępny i będę miał wystarczająco pieniędzy” moglibyśmy przedstawić symbolicznie jako (M ∧ S) → B, gdzie M = „Mam wystarczająco pieniędzy”, S = „Magazyn jest dostępny”, B = „Kupię magazyn”. Zasadniczo, symbol ∧ można po prostu czytać jako „i”. Podobnie, symbol ∨ można czytać jako „lub”, tak więc stwierdzenie „Albo pójdę pieszo, albo pojadę pociągiem do pracy, albo jedno i drugie” mogłoby być reprezentowane jako W ∨ T, gdzie W = „Pójdę pieszo do pracy”, a T = „Pojadę pociągiem do pracy”.
Używając tych symboli i kilku innych, które zostaną wprowadzone w dalszej części, możemy tworzyć dowolnie skomplikowane ciągi symboli. Istnieją dwie klasy tych ciągów symboli: te, które reprezentują prawdziwe stwierdzenia, często nazywane twierdzeniami; oraz te, które reprezentują fałszywe stwierdzenia, nazywane nietwierdzeniami. Należy zauważyć, że to, czy ciąg symboli jest twierdzeniem, czy nietwierdzeniem, zależy od tego, co oznaczają litery, więc P ∨ Q jest twierdzeniem, jeśli na przykład P reprezentuje stwierdzenie „Jest dzień”, a Q reprezentuje stwierdzenie „Jest noc” (ignorując wyjątki, takie jak zmierzch), ale byłoby nietwierdzeniem, gdyby P było „Drzewa są niebieskie”, a Q było „Wszystkie ptaki potrafią latać”. Często nazywamy ciąg symboli propozycją, jeśli nie wiemy, czy jest to twierdzenie, czy nie.
Istnieje wiele subtelności w dziedzinie logiki (w tym fakt, że kiedy mówimy „Jeśli zjesz obiad, dostaniesz deser”, faktycznie mamy na myśli „Tylko jeśli zjesz obiad, dostaniesz deser”). Jeśli ten temat Cię interesuje, istnieje wiele podręczników, które kompleksowo go omawiają.
Propozycje to typy
Zatem, biorąc pod uwagę typ a -> b, co to oznacza w kategoriach logiki symbolicznej? Bardzo zręcznie, oznacza to po prostu, że a → b. Oczywiście ma to sens tylko wtedy, gdy a i b są typami, które mogą być dalej interpretowane w naszej logice symbolicznej. To jest istota CH. Co więcej, jak wspomnieliśmy wcześniej, a → b jest twierdzeniem wtedy i tylko wtedy, gdy a -> b jest typem zamieszkanym.
Zobaczmy to na przykładzie jednej z najprostszych funkcji Haskella. Funkcja const ma typ a -> b -> a. Przetłumaczone na logikę, mamy a → b → a. Musi to być twierdzenie, ponieważ typ a -> b -> a jest zamieszkany przez wartość const. Innym sposobem wyrażenia a → b jest stwierdzenie: „Jeśli założymy, że a jest prawdziwe, to b musi być prawdziwe”. Zatem a → b → a oznacza, że jeśli założymy, że a jest prawdziwe, to jeśli dalej założymy, że b jest prawdziwe, to możemy wywnioskować a. Jest to oczywiście twierdzenie; założyliśmy a, więc a jest prawdziwe w naszych założeniach. To właśnie jest dowód – wartość const jest dowodem twierdzenia a → b → a.
Problem z ⊥
Wspomnieliśmy, że typ odpowiada twierdzeniu, jeśli ten typ jest zamieszkany. Jednak w Haskellu każdy typ jest zamieszkany przez wartość undefined. W istocie, bardziej ogólnie, cokolwiek o typie forall a. a, wartość o semantyce denotacyjnej ⊥, stanowi problem. ⊥ w teorii typów odpowiada niespójności w logice; możemy dowieść każdego twierdzenia za pomocą typów Haskella, ponieważ każdy typ jest zamieszkany. Dlatego system typów Haskella w rzeczywistości odpowiada niespójnemu systemowi logicznemu. Jednak jeśli pracujemy z ograniczonym podzbiorem systemu typów Haskella, a w szczególności wykluczymy typy polimorficzne, otrzymujemy spójny system logiczny, w którym możemy robić ciekawe rzeczy. Od tego momentu zakłada się, że pracujemy w takim systemie typów.
Teraz, gdy mamy podstawy CH, możemy zacząć nieco szerzej rozwijać związek między typami a propozycjami. Wartości w języku programowania, które odpowiadają typowi, są niczym innym, jak konstrukcjami, które realizują prawdziwość danego twierdzenia. Oznacza to, że pisząc programy, nieświadomie budujemy dowody matematyczne. To głębokie połączenie ma ogromne implikacje dla weryfikacji programów i konstrukcji języków programowania, które z natury są odporne na błędy logiczne.
Tabela porównawcza: Programowanie a Logika
Aby lepiej zrozumieć korespondencję między typami a twierdzeniami, spójrzmy na tę tabelę, która zestawia podstawowe koncepcje programowania funkcyjnego (szczególnie w kontekście języków takich jak Haskell) z ich odpowiednikami w logice intuicjonistycznej:
| Koncepcja Programistyczna (Haskell) | Koncepcja Logiczna | Wyjaśnienie |
|---|---|---|
TypA | Propozycja/Twierdzenie A | Istnienie typu oznacza, że odpowiadająca mu propozycja jest dobrze sformułowana. |
Wartość typu A | Dowód twierdzenia A | Posiadanie wartości danego typu oznacza, że twierdzenie jest prawdziwe i mamy na to konstruktywny dowód. |
Funkcja A -> B | Implikacja A → B | Funkcja, która przekształca A w B, jest dowodem na to, że jeśli A jest prawdziwe, to B jest prawdziwe. |
Para typów (A, B) | Koniunkcja A ∧ B | Jeśli mamy wartość typu (A, B), to mamy zarówno dowód A, jak i dowód B. |
Typ sumy Either A B | Dysjunkcja A ∨ B | Wartość typu Either A B oznacza, że mamy dowód A lub dowód B. |
Typ jednostkowy () / Unit | Prawda (True) / Twierdzenie zawsze prawdziwe | Typ, który ma tylko jedną możliwą wartość, reprezentuje trywialnie prawdziwe stwierdzenie. |
Typ pusty Void / Never | Fałsz (False) / Niespójność | Typ, który nie ma żadnych wartości, reprezentuje fałszywe stwierdzenie lub sprzeczność. |
Polimorfizm (np. forall a. A) | Kwantyfikacja ogólna (∀x. P(x)) | Typ, który działa dla dowolnego 'a', odpowiada twierdzeniu prawdziwemu dla wszystkich x. |
Często zadawane pytania dotyczące izomorfizmu Curry'ego-Howarda
Czym jest izomorfizm Curry'ego-Howarda w najprostszych słowach?
W najprostszych słowach, izomorfizm Curry'ego-Howarda to głębokie powiązanie między systemami typów w programowaniu a systemami dowodzenia twierdzeń w logice matematycznej. Mówi, że „typy to twierdzenia, a wartości to dowody”. Oznacza to, że każdy poprawnie skonstruowany typ danych może być interpretowany jako prawdziwe twierdzenie matematyczne, a każda wartość, która ma ten typ, jest dowodem na to twierdzenie.
Czy muszę rozumieć izomorfizm Curry'ego-Howarda, żeby programować?
Nie, absolutnie nie jest to wymagane do codziennego programowania. Większość programistów nigdy nie zagłębia się w te abstrakcyjne koncepcje. Jednak zrozumienie CH może znacząco pogłębić Twoje rozumienie systemów typów, logiki programowania i tego, dlaczego niektóre języki programowania (szczególnie te funkcyjne, takie jak Haskell) zachowują się w określony sposób. Pomaga to również w pisaniu bardziej niezawodnego i bezpiecznego kodu.
Jakie są praktyczne zastosowania izomorfizmu Curry'ego-Howarda?
CH ma kilka ważnych zastosowań, zwłaszcza w zaawansowanych dziedzinach informatyki:
- Asystenci dowodzenia (Proof Assistants): Narzędzia takie jak Coq, Agda czy Idris, które bazują na CH, pozwalają programistom pisać kod i jednocześnie formalnie udowadniać jego poprawność. Dzięki temu można tworzyć oprogramowanie o udowodnionej bezbłędności, co jest kluczowe w systemach krytycznych (np. medycznych, lotniczych).
- Weryfikacja programów: Umożliwia formalne dowodzenie właściwości programów, co jest formą ekstremalnie rygorystycznego testowania.
- Projektowanie języków programowania: Inspiruje tworzenie języków z bogatszymi systemami typów, które mogą wyrażać bardziej złożone gwarancje dotyczące zachowania programu.
- Logika i matematyka: Ułatwia automatyczne generowanie dowodów i odkrywanie nowych twierdzeń matematycznych.
Czy izomorfizm Curry'ego-Howarda dotyczy tylko Haskella?
Chociaż Haskell jest często używany jako przykład do wyjaśniania CH ze względu na jego silny i ekspresyjny system typów, sam izomorfizm jest koncepcją teoretyczną, która wykracza poza konkretny język. Dotyczy każdego języka programowania, który posiada system typów i konstrukcje logiczne. Jest szczególnie widoczny w językach programowania funkcyjnego i tych z typami zależnymi, takich jak wspomniane Coq czy Agda.
Czy CH oznacza, że programowanie to to samo co logika?
Niezupełnie. CH mówi o głębokiej korespondencji między tymi dziedzinami, ale nie o ich całkowitej tożsamości. Programowanie koncentruje się na obliczeniach i transformacji danych, podczas gdy logika formalna zajmuje się strukturą rozumowania i prawdziwością twierdzeń. CH pokazuje, że narzędzia i struktury używane w jednej dziedzinie mogą być zaskakująco przydatne do rozumienia i rozwijania drugiej. To perspektywa, która wzbogaca obie dyscypliny, pokazując ich wzajemne zależności i inspirując nowe kierunki badań i zastosowań.
Izomorfizm Curry'ego-Howarda to jedna z najpiękniejszych i najbardziej fundamentalnych idei w informatyce teoretycznej i logice matematycznej. Pokazuje, że na głębokim poziomie struktury, które tworzymy w kodzie, są odzwierciedleniem struktur logicznych, które opisują prawdę. Ta jedność pozwala na budowanie systemów, które są nie tylko funkcjonalne, ale także matematycznie poprawne i bezpieczne. Zrozumienie tego izomorfizmu otwiera drzwi do zupełnie nowego sposobu myślenia o programowaniu – nie tylko jako o sztuce tworzenia instrukcji dla maszyny, ale jako o procesie konstruowania formalnych dowodów, które świadczą o poprawności naszych pomysłów.
Zainteresował Cię artykuł Izomorfizm Curry'ego-Howarda: Typy to Twierdzenia? Zajrzyj też do kategorii Kulinaria, znajdziesz tam więcej podobnych treści!
