Programowanie funkcyjne/System typów: Różnice pomiędzy wersjami
Linia 86: | Linia 86: | ||
</p> | </p> | ||
==Reguły typowania dla prostych typów== | == Reguły typowania dla prostych typów == | ||
<p align="justify"> | |||
Sposób wnioskowania o typach możemy przedstawić w postaci reguł, | |||
<tt> | podobnych do regół dowodzenia w systemie dowodzenia. | ||
< | Nad poziomą kreską wymieniamy wszystkie przesłanki, a pod kreską wynikający z nich wniosek. | ||
W naszym przypadku, takim wnioskiem nie będzie formuła logiczna, ale stwierdzenie dotyczące typowania. | |||
</p> | |||
<p align="justify"> | |||
Oto podstawowe reguły opisujące typowanie wyrażeń. | |||
Ograniczają się one do typów monomorficznych. | |||
Nie obejmują one polimorfizmu, rekurencji i konstrukcji imperatywnych. | |||
</p> | |||
<p align="justify"> | |||
Definicje lokalne <tt>let-in</tt> traktujemy jak lukier syntaktyczny, tzn. wyrażenia postaci | |||
<tt>let <math>a = b</math> in <math>c</math></tt> | |||
rozwijamy do <tt>(function <math>a</math> -> <math>c</math>) <math>b</math> | |||
</p> | |||
<center><math> | <center><math> | ||
\frac{A \vdash e_1:\tau_1 \dots A\vdash e_n:\tau_n} | \frac{x \in dom(A)}{A \vdash x : A(x)} | ||
{A \vdash (e_1, \dots, e_n) : \tau_1 * \cdots * \tau_n} | </math></center> | ||
</math></center> | |||
<center><math> | |||
\frac{A \vdash e_1:\tau_1 \dots A\vdash e_n:\tau_n}{A \vdash (e_1, \dots, e_n) : \tau_1 * \cdots * \tau_n} | |||
</math></center> | |||
<center><math> | <center><math> | ||
\frac{}{A \vdash () : unit} | \frac{}{A \vdash () : unit} | ||
</math></center><br/> | </math></center><br/> | ||
<center><math> | <center><math> | ||
\frac{A \vdash w:\texttt{bool} A\vdash e_1:\tau A\vdash e_2:\tau}{A\vdash \texttt{if}\ w\ \texttt{then}\ e_1\ \texttt{else}\ e_2 : \tau} | \frac{A \vdash w:\texttt{bool} A\vdash e_1:\tau A\vdash e_2:\tau}{A\vdash \texttt{if}\ w\ \texttt{then}\ e_1\ \texttt{else}\ e_2 : \tau} | ||
</math></center> | </math></center> | ||
<center><math> | <center><math> | ||
\frac{A\vdash f : \tau_1 \to \tau_2 A\vdash e:\tau_1} | \frac{A\vdash f : \tau_1 \to \tau_2 A\vdash e:\tau_1}{A\vdash f\ e : \tau_2} | ||
{A\vdash f\ e : \tau_2} | |||
</math></center><br/> | </math></center><br/> | ||
<center><math> | <center><math> | ||
\frac{A, x:\tau_1 \vdash e:\tau_2} | \frac{A, x:\tau_1 \vdash e:\tau_2}{A \vdash (\texttt{function}\ x \to e) : \tau_1 \to \tau_2} | ||
{A \vdash (\texttt{function}\ x \to e) : \tau_1 \to \tau_2} | |||
</math></center><br/> | </math></center><br/> | ||
<center><math> | <center><math> | ||
\frac{A \vdash e_1:\tau_1 A, x:\tau_1 \vdash e_2:\tau_2} | \frac{A \vdash e_1:\tau_1 A, x:\tau_1 \vdash e_2:\tau_2} | ||
{A \vdash \texttt{let}\ x = e_1\ \texttt{in}\ e_2 : \tau_2} | {A \vdash \texttt{let}\ x = e_1\ \texttt{in}\ e_2 : \tau_2} | ||
</math></center><br/> | </math></center><br/> | ||
<center><math> | <center><math> | ||
\frac{A\vdash e:\tau}{A\vdash (e) : \tau} | \frac{A\vdash e:\tau}{A\vdash (e) : \tau} |
Wersja z 11:50, 1 paź 2006
Wstęp
W tym wykładzie zamiemy się systemem typów w Ocamlu oraz zasadom, na podstawie których kompilator wnioskuje o typach. Ograniczymy się przy tym do podstawowych konstrukcji języka i uprościmy trochę składnię.
<wyrażenie> ::= <identyfikator> | <konstruktor> | 0 | 1 | ... ( <wyrażenie> { , <wyrażenie> }* ) | if <wyrażenie> then <wyrażenie> else <wyrażenie> | <wyrażenie> <wyrażenie> | function <identfikator> <wyrażenie> | let <identyfikator> = <wyrażenie> in <wyrażenie> | (<wyrażenie>) <konstruktor> ::= <Identyfikator> { <wyrażenie> }*
Niektóre konstruktory i stałe mają specjalną składnię, np. konstruktor -tki i operacje arytmetyczne. Gdy tylko nie będzie to budzić niejednoznaczności, a będzie to przydatne, będziemy traktowali konstruktory również jako nazwy procedur konstruujących wartości. Uprości to nasze rozważania, nie zmniejszając ich ogólności.
Typy i środowiska typujące
Będą nam potrzebne pojęcia typu prostego i schematu typów. Przez typy proste bedziemy rozumieli napisy postaci:
<typ prosty> ::= <zmienna typowa> | ( <typ prosty> ) | <typ prosty> -> <typ prosty> | <typ prosty> { * <typ prosty> }* | <typ prosty>1 <typ prosty>2 ... <typ prosty>n <konstruktor typu>n <konstruktor typu>0 ::= int | bool | float | unit | ... <konstruktor typu>1 ::= list | ... <zmienna typowa> ::= <zmienna typowa silna> | <zmienna typowa słaba> <zmienna typowa silna> ::= 'a | 'b | 'c | ... <zmienna typowa słaba> ::= '_a | '_b | '_c | ...
Przyjmujemy, że konstruktory typów wiążą najsilniej, * słabiej, a -> najsłabiej. Przy tym -> jest lewostronnie łączne.
Przez schematy typów będziemy rozumieli napisy następującej postaci:
<schemat typu> ::= [ <zmienna typowa silna> { , <zmienna typowa silna> }* ] . <typ prosty> | <typ prosty>
Typ prosty opisuje jeden typ, zależny od wartości ew. zawartych w nim zmiennych typowych. Schemat typów opisuje te typy, do których potrafi się przystosować wartość polimorficzna. Wymagamy przy tym, aby wszystkie silne zmienne typowe były związane pod kwantyfikatorem. Schemat typu 'a,'b,...,'g. możemy rozumieć jako opis zbioru wszystkich tych typów, które możemy uzyskać podstawiając w za 'a,'b,...,'g (równocześnie) dowolne typy. Silne zmienne typowe są też nazywane polimorficznymi, gdyż opisują to w jaki sposób wartości polimorficzne mogą być wielu typów.
Słabe zmienne typowe zostały wprowadzone ze względu na problemy na styku konstrukcji imperatywnych i polimorfizmu. Słaba zmienna typowa opisuje typ, który nie do końca jest znany. W szczególności, przy inkrementacyjnym trybie pracy z kompilatorem, słabe zmienne typowe mogą w trakcie interakcji być zastępowane konkretniejszymi typami. Tak więc nie opisują one polimorfizmu, lecz nie do końca rozpoznany monomorfizm. Nie mogą również być kwantyfikowane w schematach typów. Są one też nazywane monomorficznymi zmiennymi typowymi. Zajmiemy się dokładnie nimi dalej, gdy będziemy omawiać problemy na styku polimorfizmu i konstrukcji imperatywnych.
Będzie nam jeszcze potrzebne pojęcie środowiska typującego. Środowisko typujące, to skończona funkcja (mapa) przypisująca identyfikatorom typy (proste, lub schematy, w zależności od potrzeby). Przechowuje ono informacje potrzebne kompilatorowi do wyznaczania typów. Zakładamy istnienie takiego środowiska typującego wszystkie stałe i konstruktory wbudowane w język. Oczywiście typy tych stałych i konstruktorów muszą być zgodne z ich arnościami.
Niech oznacza środowisko typujące. Przez będziemy oznaczać środowisko identyczne z , z tym wyjątkiem, że nazwie przyporządkowuje typ . Typowanie wyrażeń w programie będziemy oznaczali przez stwierdzenia postaci: , co można rozumieć następująco: "zakładając typowania ze środowiska możemy stwierdzić, że jest typu .
Reguły typowania dla prostych typów
Sposób wnioskowania o typach możemy przedstawić w postaci reguł, podobnych do regół dowodzenia w systemie dowodzenia. Nad poziomą kreską wymieniamy wszystkie przesłanki, a pod kreską wynikający z nich wniosek. W naszym przypadku, takim wnioskiem nie będzie formuła logiczna, ale stwierdzenie dotyczące typowania.
Oto podstawowe reguły opisujące typowanie wyrażeń. Ograniczają się one do typów monomorficznych. Nie obejmują one polimorfizmu, rekurencji i konstrukcji imperatywnych.
Definicje lokalne let-in traktujemy jak lukier syntaktyczny, tzn. wyrażenia postaci let in rozwijamy do (function -> )
Przykład
Pokazać, że:
let id = (function x -> x) in id 42 : int.
Przykład
Pokazać, że:
let id = (fun x -> x) in id id
nie da się otypować (brak polimorfizmu).
Polimorfizm
Chcąc wyrazić, że jakieś wyrażenie (np. id w powyższym przykładzie) może przyjmować różne typy w różnych wystąpieniach, musimy użyć schematów typów. Taki schemat powinien opisywać wszystkie możliwe typy wartości polimorficznej.
Fakt
Równocześnie powinniśmy być w stanie ukonkretniać ten typ na różne sposoby.
Przykład
Pokazać, że let id = (fun x -> x) in id id jest typu .
Zauważmy, że mamy polimorficzną regułę dla let-a, ale nie mamy polimorficznej reguły dla -abstrakcji i aplikacji funkcji.
Przykład
Pokazać, że nie da się otypować (function id -> id id)(function x -> x).\end{przyklad}
Rekurencja
Jak możemy wnioskować o typie procedur rekurencyjnych? Sięgnijmy po technikę podobną do prezentowanej wcześniej. Definicję rekurencyjną możemy wyrazić jako punkt stały. Załóżmy, że środowisko typujące zawiera operator punktu stałego:
Definicję rekurencyjną postaci:
zamieniamy na:
Zwróćmy uwagę, że wewnątrz definicji f funkcja ta niejest polimorficzna.
Wyjątki
Uproszczona wersja --- nie wnikamy we wzorce.
Konstrukcje imperatywne i słabe zmienne typowe
Dla sekwencji instrukcji imperatywnych mamy regułę:
Gdyz tak naprawdę ; możemy zdefiniować jako:
let (;) x y = y;;
Gdzie jest problem?
let r = ref (function x -> x);;
Powinniśmy być w stanie pokazać, że:
Jeśli się zastanowimy jakiego typu są := i !, to jedyne sensowne typy, to:
Czyli, np.:
To jednak nie ma sensu!
Rozwiązaniem są tzw. słabe zmienne typowe, oznaczane jako :
- Słabe zmienne typowe nie są kwantyfikowane.
- Za słabe zmienne typowe (w trakcie rekonstrukcji typów) można podstawić typy proste zawierające tylko słabe zmienne typowe.
- Każda próba podstawienia za słabą zmienną typową jakiegoś typu powoduje, że na stałe zostaje ona zastąpiona tym typem. (Top level.)
- Uwaga: Kompilator może nazywać różne słabe zmienne typowe tak samo. Trzeba sobie zdawać sprawę kiedy dwie zmienne typowe są różne.
- Przy deklaracji let, typ stałej jest kwantyfikowany tylko wtedy, gdy wyrażenie definiujące stałą nie jest ekspansywne.
- Referencje i aplikacje są ekspansywne.
let r = ref (function x -> x);; val r : ('_a -> '_a) ref = {contents = <fun>}r := (function x -> 42);; r : (int -> int) ref let id = (function x -> x);; val id : 'a -> 'a = <fun> let idd = id id;; val idd : '_a -> '_a = <fun> let idd x = id id x;; val idd : 'a -> 'a = <fun> let p = id id;; val p : '_a -> '_a let r = ref p;; val r : ('_a -> '_a) ref p 42;; p: int -> int'r: (int -> int) ref let r = ref id;; val r : ('_a -> '_a) ref let q = ref id;; val q : ('_a -> '_a) ref !r 42 r: (int -> int) ref val q : ('_a -> '_a) ref let r = ref id;; val r : ('_a -> '_a) ref let q = ref (!r);; val q : ('_a -> '_a) ref !r 42;; r: (int -> int) ref q: (int -> int) ref let f x = let r = ref id in (r := compose !r !r; !r x);; val f : 'a -> 'a f 42;; = 42 f true;; = true let g = let r = ref id in (r := compose !r !r; !r);; val g : '_a -> '_a g 42;; = 42 g : int -> int let f x = let r = ref x in (function y -> r := compose !r !r; !r y);; [.....] let f x = let r = ref x in (function y -> r := y; !r);; val f : 'a -> 'a -> 'a
Jeśli zdefiniujemy procedurę, której typ zawiera słabe zmienne typowe, a chcemy, żeby miała silne, to możemy zastosować -ekspansję, tzn.\ dodać formalny argument procedury. Jeżeli nie ma (imperatywnych) przeciwwskazań, to uzyskamy procedurę równoważną.