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>1 <typ prosty>2 ... <typ prosty>n  <konstruktor typu>n
<konstruktor typu>0    ::= int | bool | float | ...
<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, być może zależny od wartości zawartych w nim zmiennych typowych. System typów bez polimorfizmu można opisać używając typów prostych. Jednak, gdy chcemy ująć wszystkie typy, do których potrafią się dostosować wartości polimorficzne, potrzebujemy bardziej ogólnej konstrukcji -- schematu typów.

Schemat typu α1,,αn.τ możemy rozumieć jako opis zbioru wszystkich typów, jakie możemy uzyskać podstawiając w τ za α1,,αn dowolne typy. Środowisko typujące, to skończona funkcja (mapa) przypisująca identyfikatorom typy (proste, lub schematy, w zależności od potrzeby). 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 oznacza, że zakładając typowania ze środowiska A możemy stwierdzić, że e jest typu τ.

Reguły typowania dla prostych typów

Bez polimorfizmu. Tylko monomorfizm. Let jest traktowany wyłącznie jak lukier syntaktyczny. Środowisko typujące używa wyłącznie typów prostych.

xdom(A)Ax:A(x)


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


A():unit


Aw:boolAe1:τAe2:τAif w then e1 else e2:τ


Af:τ1τ2Ae:τ1Af e:τ2


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


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


Ae:τA(e):τ



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

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