Programowanie funkcyjne/System typów

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania

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 n-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.t możemy rozumieć jako opis zbioru wszystkich tych typów, które możemy uzyskać podstawiając w t 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 A oznacza środowisko typujące. Przez A,x:τ będziemy oznaczać środowisko identyczne z A, z tym wyjątkiem, że nazwie x przyporządkowuje typ τ. Typowanie wyrażeń w programie będziemy oznaczali przez stwierdzenia postaci: Ae:τ, co można rozumieć następująco: "zakładając typowania ze środowiska A możemy stwierdzić, że 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. Tak więc chwilowo pomijamy (słabe i silne) zmienne typowe.

Definicje lokalne let-in traktujemy tu jak lukier syntaktyczny, tzn. wyrażenia postaci let a=b in c rozwijamy do (function a -> c) b

Można wywnioskować wszystkie typy określone w środowisku.

xdom(A)Ax:A(x)


Jeśli znane są typy współrzędnych n-ki, to jej typ jest odpowiednim typem produktowym.

Ae1:τ1, , Aen:τnA(e1,,en):τ1**τn


Pusta n-ka () jest zawsze typu unit.

A():unit



W wyrażeniu warunkowym if-then-else pierwszy człon musi być typu bool, a dwa pozostałe muszą być tego samego typu. Taki też jest typ całego wyrażenia.

Aw:bool, Ae1:τ, Ae2:τAif w then e1 else e2:τ


Jeśli znany jest typ procedury i typ argumentu pasuje do typu procedury, to wynik zastosowania procedury do argumentu ma taki typ, jak to wynika z typu procedury.

Parser nie mógł rozpoznać (nieznana funkcja „\begin{array}”): {\displaystyle \begin{array}{c} A\vdash f : \tau_1 \to \tau_2,\ A\vdash e:\tau_1 \\\hline A\vdash f\ e : \tau_2} \end{array} }



Jeżeli, przy założeniu, że parametr λ-abstrakcji jest typu τ1, można pokazać, że wynik λ-abstrakcji jest typu τ2, to sama λ-abstrakcja jest typu τ1τ2.

A,x:τ1e:τ2A(function xe):τ1τ2



Jeżeli możemy wywnioskować jaki jest typ wartości definiowanej lokalnie, to typ tej wartości możemy umieścić w środowisku typującym i o typie wyrażenia let-in wnioskować na podstawie tak wzbogaconego środowiska i drugiego członu wyrażenia.

Ae1:τ1A,x:τ1e2:τ2Alet x=e1 in e2:τ2


Nawiasy nie zmieniają typowania.

Ae:τA(e):τ



Przykład Typowanie

Oto wywód tego, że:

let id = (function x -> x) in id 42

jest typu int.

Parser nie mógł rozpoznać (nieznana funkcja „\def”): {\displaystyle \def\rule#1#2{\begin{array}[b]{c}#1\\\hline#2\end{array}} \def\int{\mathtt{int}} \rule{ \rule{ \rule{ }{ 42:\int, \texttt{x}:\int \vdash \texttt{x}:\int } }{ 42:\int \vdash \texttt{(function x -> x)}:\int \to \int } ~~ \rule{ \rule{ }{ 42:\int, \texttt{id}:\int\to\int \vdash \texttt{id}:\int\to\int }~~ \rule{ }{ 42:\int, \texttt{id}:\int\to\int \vdash 42:\int } }{ 42:\int, \texttt{id}:\int \to \int \vdash \texttt{id 42}:\int } }{ 42:\int \vdash \texttt{let id = (function x -> x) in id 42}:\int } }

W powyższym typowaniu procedura id miała typ int -> int, choć mogłaby mieć dowolny typ pasujący do schematu α.αα. Jednak jakiego byśmy typu dla id nie przyjeli, to wszystkie wystąpienia tej procedury po prawej stronie in muszą mieć ten sam typ. Tak więc, np. dla wyrażenia:

let id = (fun x -> x) in id id

nie da się wyznaczyć żadnego typu. Żeby było to możliwe, potrzebne będzie wprowadzenie schematów typów i 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

Dla każdego wyrażenia istnieje (najbardziej ogólny) schemat typu, który obejmuje wszystkie typy, które można wyprowadzić dla tego wyrażenia.

Równocześnie powinniśmy być w stanie ukonkretniać ten typ na różne sposoby.

xdom(A),A(x)=α.τAx:τ[π/α]


Ae1:τ1A,x:Free(τ1)Free(A).τ1e2:τ2Alet x=e1 in e2:τ2


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:

A(fix)=α.(αα)α


Definicję rekurencyjną postaci:

let rec f x=e1 in e2


zamieniamy na:

let f=fix(function ffunction xe1) in e2


Zwróćmy uwagę, że wewnątrz definicji f funkcja ta niejest polimorficzna.

Wyjątki

Ae:exnAraise e:τ


Uproszczona wersja --- nie wnikamy we wzorce.

Ae1:τA,x:exne2:τAtry e1 with xe2:τ

Konstrukcje imperatywne i słabe zmienne typowe

Dla sekwencji instrukcji imperatywnych mamy regułę:

Ae1:τ1Ae2:τAe1;e2:τ


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:

r:α.(αα) ref


Jeśli się zastanowimy jakiego typu są := i !, to jedyne sensowne typy, to:

A:=:α.α ref α unit


A!:α.α ref α


Czyli, np.:

A,r:α.(αα) ref(r:=(function xx+42))


A,r:α.(αα) ref!r true:bool


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ą.

Laboratoria i ćwiczenia

Laboratoria i ćwiczenia do wykładu