Wykład 3 Funkcje wyższych rzędów Funkcje jako dane Rozwijanie i zwijanie funkcji Składanie funkcji Funkcjonały dla list Funkcje wyższego rzędu jako struktury sterowania Semantyka denotacyjna prostego języka imperatywnego Rekonstrukcja typu Zdzisław Spławski Programowanie funkcyjne 1
Funkcje jako dane Języki programowania funkcyjnego traktują funkcje tak samo jak obiekty danych innych typów, a więc możliwe są: funkcje, produkujące funkcje jako wyniki; funkcje jako argumenty innych funkcji; struktury danych (np. krotki, listy) z funkcjami jako składowymi. Funkcja operująca na innych funkcjach jest nazywana funkcją wyższego rzędu lub funkcjonałem (ang. functional, stąd functional programming, a po polsku programowanie funkcyjne lub funkcjonalne). Zdzisław Spławski Programowanie funkcyjne 2
Funkcje jako dane Funkcje są często przekazywane jako dane w obliczeniach numerycznych. m Poniższy funkcjonał oblicza sumę 1 f ( i). Dla efektywności wykorzystuje on i= 0 rekursję ogonową. let sigma f m = let rec suma (i,z)= if i=m then z else suma(i+1, z+.(f i)) in suma(0, 0.0) ;; val sigma : (int -> float) -> int -> float = <fun> W połączeniu z funkcjonałami wygodne okazuje się wykorzystanie funkcji 9 2 anonimowych. Możemy obliczyć np. k =0 k bez konieczności oddzielnego definiowania funkcji podnoszenia do kwadratu. sigma (function k -> float(k*k)) 10;; - : float = 285. Zdzisław Spławski Programowanie funkcyjne 3
Funkcje jako dane 3 4 Wykorzystując ten funkcjonałłatwo obliczyć np. i j. = = + i 0 j 0 # sigma (function i -> sigma (function j -> float(i+j)) 5) 4;; -: float = 70. W przypadku częstego sumowania elementów macierzy można łatwo uogólnić m to na 1 n 1 g( i, j), czyli i= 0 j= 0 # let sigma2 g m n = sigma (function i -> sigma (function j -> g(i,j)) n) m;; val sigma2 : (int * int -> float) -> int -> int -> float = <fun> Wartość 3 4 = = i + i 0 j 0 j można teraz wyliczyć prościej: # sigma2 (function (k,l) -> float(k+l)) 4 5;; - : float = 70. Zdzisław Spławski Programowanie funkcyjne 4
Rozwijanie i zwijanie funkcji Na wykładzie 2. była mowa o postaci rozwiniętej (ang. curried) i zwiniętej (ang. uncurried) funkcji. Możemy napisać funkcjonał curry (odp. uncurry), który bierze funkcję w postaci zwiniętej (odp. rozwiniętej) i zwraca tę funkcję w postaci rozwiniętej (odp. zwiniętej). # let curry f x y = f (x, y);; (* Rozwijanie funkcji *) (* let curry = function f -> function x -> function y -> f (x,y);;*) val curry : ('a * 'b -> 'c) -> 'a -> 'b -> 'c = <fun> # let uncurry f (x, y) = f x y;; (* Zwijanie funkcji *) val uncurry : ('a -> 'b -> 'c) -> 'a * 'b -> 'c = <fun> # let plus (x,y) = x+y;; val plus : int * int -> int = <fun> # curry plus 4 5;; - : int = 9 # let add x y = x+ y;; (* lub let add = curry plus;; *) val add : int -> int -> int = <fun> # uncurry add (4,5);; - : int = 9 Zdzisław Spławski Programowanie funkcyjne 5
Składanie funkcji W matematyce często jest używana operacja złożenia (superpozycji) funkcji. Niech będą dane funkcje f :α β oraz g:γ α. Złożenie (f g):γ β jest definiowane (f g) (x) = f (g x). # let ($$) f g = fun x -> f ( g x);; (* Składanie funkcji *) val ( $$ ) : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = <fun> # let sqr x = x*x;; val sqr : int -> int = <fun> # (sqr $$ sqr) 2;; -: int = 16 # let third l3 = (List.hd $$ List.tl $$ List.tl) l3;; val third : 'a list -> 'a = <fun> # third [1;2;3;4;5];; -: int = 3 # let next_char = Char.chr $$ (+)1 $$ Char.code;; val next_char : char -> char = <fun> # next_char 'f';; - : char = 'g' Zdzisław Spławski Programowanie funkcyjne 6
Funkcjonały dla list - map Łącząc funkcje wyższych rzędów i parametryczny polimorfizm można pisać bardzo ogólne funkcjonały. Funkcjonał map aplikuje funkcję do każdego elementu listy: map f [x 1 ;... ; x n ]-> [f x 1 ;... ; f x n ] # let rec map f = function [] -> [] x::xs -> (f x) :: map f xs ;; val map : ('a -> 'b) -> 'a list -> 'b list = <fun> # map (function x -> x*x) [1;2;3;4];; -: int list = [1; 4; 9; 16] Ten funkcjonał jest zdefiniowany w module List: # List.map;; -: f:('a -> 'b) -> 'a list -> 'b list = <fun> # List.map String.length ["Litwo";"ojczyzno";"moja"];; - : int list = [5; 8; 4] Zdzisław Spławski Programowanie funkcyjne 7
Funkcjonały dla list - filter Funkcjonał filter aplikuje predykat do każdego elementu listy; jako wynik zwraca listę elementów spełniających ten predykat w oryginalnym porządku. # let rec filter pred = function [] -> [] x::xs -> if pred x then x::filter pred xs else filter pred xs ;; val filter : ('a -> bool) -> 'a list -> 'a list = <fun> Efektywniejszą implementację (z wykorzystaniem rekursji ogonowej ) można znaleźć w module List. # List.filter (function s -> String.length s <= 6) ["Litwo";"ojczyzno";"moja"];; - : string list = ["Litwo"; "moja"] Zdzisław Spławski Programowanie funkcyjne 8
Filter z rekursją ogonową # let filter_bad p = let rec find acc = function [] -> acc x :: xs -> if p x then find (acc@[x]) xs else find acc xs in find [];; val filter_bad : ('a -> bool) -> 'a list -> 'a list = <fun> (* Złożoność O(n 2 ). Nigdy w ten sposób! *) let filter p = let rec find acc = function [] -> List.rev acc x :: xs -> if p x then find (x :: acc) xs else find acc xs in find [];; (* Złożoność O(2n). Tylko tak! *) Zdzisław Spławski Programowanie funkcyjne 9
Generowanie liczb pierwszych metodą sita Eratostenesa Utwórz ciąg skończony [2,3,4,5,6,...,n]. Dwa jest liczbą pierwszą. Usuń z ciągu wszystkie wielokrotności dwójki, ponieważ nie mogą być liczbami pierwszymi. Pozostaje ciąg [3,5,7,9,11,...]. Trzy jest liczbą pierwszą. Usuń z ciągu wszystkie wielokrotności trójki, ponieważ nie mogą być liczbami pierwszymi. Pozostaje ciąg [5,7,11,13,17,...]. Pięć jest liczbą pierwszą itd. Na każdym etapie ciąg zawiera liczby mniejsze lub równe n, które nie są podzielne przez żadną z wygenerowanych do tej pory liczb pierwszych, więc pierwsza liczba tego ciągu jest liczbą pierwszą. Powtarzając opisane kroki otrzymamy wszystkie liczby pierwsze z zakresu [2..n]. # let primes to_n = let rec sieve n = if n <= to_n then n::sieve(n+1) else [] and find_primes = function h::t -> h:: find_primes (List.filter (function x -> x mod h <> 0) t) [] -> [] in find_primes(sieve 2);; val primes : int -> int list = <fun> # primes 30;; - : int list = [2; 3; 5; 7; 11; 13; 17; 19; 23; 29] Zdzisław Spławski Programowanie funkcyjne 10
Funkcjonały dla list - insert Funkcjonał insert bierze funkcję poprzedza (w postaci rozwiniętej) zadającą porządek elementów w liście, wstawiany element elem oraz listę uporządkowaną i wstawia elem do listy zgodnie z zadanym porządkiem. # let rec insert poprzedza elem = function [] -> [elem] h::t as l -> if poprzedza elem h then elem::l else h::(insert poprzedza elem t);; val insert : ('a -> 'a -> bool) -> 'a -> 'a list -> 'a list = <fun> Funkcję insert łatwo wyspecjalizować dla zadanego porządku, np. # let insert_le elem = insert (<=) elem;; val insert_le : 'a -> 'a list -> 'a list = <fun> # insert_le 4 [1;2;3;4;5;6;7;8];; - : int list = [1; 2; 3; 4; 4; 5; 6; 7; 8] Zdzisław Spławski Programowanie funkcyjne 11
Funkcjonały dla list uogólnienie typowych funkcji # let rec sumlist l = match l with h::t -> h + sumlist t (* funkcja f, której argumentami są głowa listy h i wynik wywołania rekurencyjnego definiowanej funkcji na ogonie t *) [] -> 0;; (* wynik acc definiowanej funkcji dla listy pustej [] *) val sumlist : int list -> int = <fun> Analogiczną strukturę miało wiele rozważanych przez nas funkcji na listach. Uogólniając tę funkcję i umieszczając f i acc jako dodatkowe argumenty otrzymujemy poniższy funkcjonał: # let rec fold_right f l acc = match l with h::t -> f h (fold_right f t acc) [] -> acc;; val fold_right : ('a -> 'b -> 'b) -> 'a list -> 'b -> 'b = <fun> Funkcję sumlist możemy teraz zdefiniować wykorzystując funkcjonał fold_right: # let sumlist l = fold_right (+) l 0;; val sumlist : int list -> int = <fun> Zdzisław Spławski Programowanie funkcyjne 12
Funkcjonały dla list fold_left (accumulate) fold_right (reduce) Funkcjonał fold_left aplikuje dwuargumentową funkcję f do każdego elementu listy (z lewa na prawo) akumulując wynik w acc (efektywnie rekursja ogonowa): fold_left f acc [x 1 ;... ; x n ]-> f(...(f(f acc x 1 )x 2 )...)x n # let rec fold_left f acc = function h::t -> fold_left f (f acc h) t [] -> acc;; val fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a = <fun> Dualny funkcjonał fold_right aplikuje dwuargumentową funkcję f do każdego elementu listy (z prawa na lewo) akumulując wynik w acc (zwykła rekursja): fold_right f [x 1 ;... ; x n ] acc -> f x 1 (f x 2 (...(f x n acc)...)) # let rec fold_right f l acc = match l with h::t -> f h (fold_right f t acc) [] -> acc;; val fold_right : ('a -> 'b -> 'b) -> 'a list -> 'b -> 'b = <fun> Moduł List zawiera wiele użytecznych funkcjonałów dla list. Zdzisław Spławski Programowanie funkcyjne 13
fold_left (accumulate) - wykorzystanie # let sumlist = fold_left (+) 0;; (* sumowanie elementów listy *) val sumlist : int list -> int = <fun> # sumlist [4;3;2;1];; - : int = 10 # let prodlist = fold_left ( * ) 1;; (* mnożenie elementów listy *) val prodlist : int list -> int = <fun> # prodlist [4;3;2;1];; -: int = 24 # let flat l = fold_left (@) [] l;; (* spłaszczanie list *) val flat : 'a list list -> 'a list = <fun> # flat [[5;6];[1;2;3]];; -: int list = [5; 6; 1; 2; 3] (* konkatenowanie elementów listy napisów *) # let implode = fold_left (^) "";; val implode : string list -> string = <fun> # implode ["Ala ";"ma ";"kota"];; - : string = "Ala ma kota" Zdzisław Spławski Programowanie funkcyjne 14
Abstrakcja pomaga zauważyć i wyrazić analogie Bez wykorzystania odpowiednio wysokiego poziomu abstrakcji funkcyjnej trudno byłoby zauważyć analogie między funkcjami z poprzedniego slajdu. Struktura funkcji jest identyczna, ponieważ w każdym wypadku mamy do czynienia z monoidem: liczby całkowite, +, 0 liczby całkowite, *, 1 listy, @, [] napisy, ^, "" Operacja monoidu jest łączna, więc w omawianych wyżej funkcjach moglibyśmy użyć funkcjonału fold_right, np. # let imploder l = List.fold_right (^) l "";; val imploder : string list -> string = <fun> # imploder ["Ala ";"ma ";"kota"];; - : string = "Ala ma kota" Zdzisław Spławski Programowanie funkcyjne 15
Przykłady algebr abstrakcyjnych (homogenicznych) Półgrupa S, : S S S a (b c)=(a b) c (łączność) Monoid S,, 1 jest półgrupą z obustronną jednością (elementem neutralnym) 1:S a 1=a 1 a=a Grupa S,,, 1 jest monoidem, w którym każdy element posiada element odwrotny względem binarnej operacji monoidu : S S a a=1 a a=1 Zdzisław Spławski Programowanie funkcyjne 16
Matematykiem jest, kto umie znajdować analogie między twierdzeniami; lepszym, kto widzi analogie dowodów, i jeszcze lepszym, kto dostrzega analogie teorii, a można wyobrazić sobie i takiego, co między analogiami widzi analogie. Stefan Banach Zdzisław Spławski Programowanie funkcyjne 17
Funkcje wyższego rzędu jako struktury sterowania int silnia (int n) { int x,y; x=n; y=1; // utworzenie pary <n,1> while (x!= 0) // n-krotna iteracja { y = x*y; // operacji f takiej, x = x-1; // że f<x,y> = <x-1,x*y> } // return y; // wydzielenie drugiego elementu pary } Wykorzystując nieformalne komentarze, możemy formalnie zapisać tę funkcję w języku OCaml (z rekursją ogonową, co jest naturalne zważywszy, że komentarze dotyczą wersji iteracyjnej). let silnia' n = let rec f(x,y) = if x<>0 then f(x-1,x*y) else (x,y) in snd (f(n,1));; # silnia' 5;; - : int = 120 Zdzisław Spławski Programowanie funkcyjne 18
Składnia i semantyka prostego języka imperatywnego PJI Składnia abstrakcyjna V Zmienna N Liczba B ::= true false B && B B B not B E < E E = E E ::= N V E + E E * E E E -E C ::= skip C; C V := E if B then C else C fi while B do C od Semantykę denotacyjną języka zadaje się definiując funkcje semantyczne, które opisują, jak semantyka wyrażenia może być otrzymana z semantyki składowych tego wyrażenia (semantyka kompozycyjna). Funkcja semantyczna dla instrukcji C jest funkcja częściową «C, zmieniającą wektor stanu programu S (pamięć operacyjną). Dziedzinę S reprezentujemy za pomocą zbioru funkcji σ: Zmienna Z. Załóżmy dla uproszczenia, że funkcje semantyczne dla wyrażeń logicznych B i arytmetycznych E są znane. «B : S {true, false} «E : S Z Zdefiniujemy funkcję semantyczną «C : S Ã S dla instrukcji, a potem (pomijając pewne szczegóły formalne), zaprogramujemy ja w OCamlu. Zdzisław Spławski Programowanie funkcyjne 19
Składnia i semantyka prostego języka imperatywnego PJI Składnia abstrakcyjna C ::= skip C; C V := E if B then C else C fi while B do C od Semantyka denotacyjna «B : S {true, false} «E : S Z «skip σ = σ «C 1 ; C 2 σ '«C 2 («C 1 σ) Powyżej są używane dwa funkcjonały F : [ S Ã S ] [ S Ã S ] i fix : [[ S Ã S ] [ S Ã S ]] [ S Ã S ], znajdujący najmniejszy punkt stały zadanego funkcjonału. Jego istnienie gwarantuje teoria dziedzin. Zdzisław Spławski Programowanie funkcyjne 20
Funkcjonał dla punktu stałego W języku z mocną typizacją i wartościowaniem gorliwym (Ocaml, SML) można zdefiniować funkcjonał, znajdujący punkty stałe funkcji. W językach z mocną typizacją i wartościowaniem leniwym można zdefiniować ogólniejszy funkcjonał fix : ('a 'a) 'a, znajdujący punkty stałe np. list. # let fix f = let rec fixf x = f fixf x in fixf;; val fix : (('a -> 'b) -> 'a -> 'b) -> 'a -> 'b = <fun> # let f = fun f n -> if n=0 then 1 else n*f(n-1);; val f : (int -> int) -> int -> int = <fun> # let fact = fix f;; val fact : int -> int = <fun> # fact 3;; - : int = 6 # fact 4;; - : int = 24 Jak widać, najmniejszym punktem stałym funkcjonału f, skonstruowanym przez fix jest funkcja silnia. O punktach stałych i funkcjonałach, znajdujących punkty stałe, będzie mowa pod koniec semestru przy omawianiu rachunku lambda. Zdzisław Spławski Programowanie funkcyjne 21
Funkcje semantyczne dla PJI # let svar v s = List.assoc v s;; val svar : 'a -> ('a * 'b) list -> 'b = <fun> # let sskip s = s;; val sskip : 'a -> 'a = <fun> # let sscolon instr1 instr2 s = instr2 (instr1 s) (* czyli instr2 $$ instr1 *);; val sscolon : ('a -> 'b) -> ('b -> 'c) -> 'a -> 'c = <fun> # let sass var expr s = (var, expr)::(list.remove_assoc var s);; val sass : 'a -> 'b -> ('a * 'b) list -> ('a * 'b) list = <fun> # let sif b instr1 instr2 s = if b s then instr1 s else instr2 s;; val sif : ('a -> bool) -> ('a -> 'b) -> ('a -> 'b) -> 'a -> 'b=<fun> # let swhile b instr s = (fix (fun f b instr s -> if b s then f b instr (instr s) else s)) b instr s;; val swhile : ('a -> bool) -> ('a -> 'a) -> 'a -> 'a = <fun>. Zdzisław Spławski Programowanie funkcyjne 22
Semantyka denotacyjna silni Poniżej przedstawiono semantykę denotacyjną silni (cum grano salis :) # let call n = ("n",n)::("x",0)::("y",0)::[];; val call : int -> (string * int) list = <fun> # let sassx1 s = sass "x" (svar "n" s) s;; val sassx1 : (string * 'a) list -> (string * 'a) list = <fun> # let sassy1 s = sass "y" 1 s;; val sassy1 : (string * int) list -> (string * int) list = <fun> # let sloop = let sassy2 s = sass "y" ((svar "x" s)*(svar "y" s)) s and sassx2 s = sass "x" ((svar "x" s)-1) s in swhile (fun m -> (svar "x" m) <> 0) (sscolon sassy2 sassx2);; val sloop : (string * int) list -> (string * int) list = <fun> # let silnia = (svar "y") $$ sloop $$ sassy1 $$ sassx1 $$ call;; val silnia : int -> int = <fun> # silnia 3;; - : int = 6 # silnia 4;; - : int = 24 # silnia 5;; - : int = 120 Zdzisław Spławski Programowanie funkcyjne 23
# let f z y x = y (y z x);; Rekonstrukcja typu val f : 'a -> ('a -> 'b -> 'a) -> 'b -> 'b -> 'a = <fun> Skąd kompilator wziął ten typ? Formalnie typ powstał w wyniku rozwiązania układu równań w pewnej algebrze typów. Na podstawie ilości argumentów widzimy, że musi być f : α β γ δ. Z kontekstu użycia argumentu y:β widzimy, że y jest funkcją i otrzymujemy dwa równania: β=α γ ϕ oraz β=ϕ δ w których przez ϕ oznaczyliśmy typ wyniku funkcji y. Oczywiście β=α (γ ϕ)=ϕ δ, skąd otrzymujemy: α=ϕ oraz δ=γ ϕ=γ α czyli β=α γ α. Na argumenty x i z kontekst nie nakłada żadnych ograniczeń, więc ostatecznie f : α (α γ α) γ γ α. W praktyce do rekonstrukcji typu kompilator wykorzystuje algorytm unifikacji. Zdzisław Spławski Programowanie funkcyjne 24
Zadania kontrolne (1) 1. Podaj typy poniższych funkcji: a) let f1 x = x 1 1;; b) let f2 x y z = x ( y ^ z );; c) let f3 x y z = x y z;; d) let f4 x y = function z -> x::y;; 2. Zdefiniuj funkcje curry3 i uncurry3, przeprowadzające konwersję między zwiniętymi i rozwiniętymi postaciami funkcji od trzech argumentów. Podaj ich typy. 3. Dodajmy do języka PJI poniższe trzy rodzaje pętli, których semantyka znana jest intuicyjnie z języka Pascal. Zdefiniuj semantykę denotacyjną dla tych pętli, a potem zdefiniuj te funkcje semantyczne w OCamlu. a) repeat C until B b) for V:=E to E do C c) for V:=E to E do C 4. Przekształć poniższą rekurencyjną definicję funkcji sumprod, która oblicza jednocześnie sumę i iloczyn listy liczb całkowitych na równoważną definicję nierekurencyjną z jednokrotnym użyciem funkcji fold_left. let rec sumprod = function h::t -> let (s,p)= sumprod t in (h+s,h*p) [] -> (0,1);; Zdzisław Spławski Programowanie funkcyjne 25
Zadania kontrolne (2) 5. Poniższe dwie wersje funkcji quicksort działają niepoprawnie. Dlaczego? let rec quicksort = function [] -> [] [x] -> [x] xs -> let small = List.filter (fun y -> y < List.hd xs ) xs and large = List.filter (fun y -> y >= List.hd xs ) xs in quicksort small @ quicksort large;; let rec quicksort' = function [] -> [] x::xs -> let small = List.filter (fun y -> y < x ) xs and large = List.filter (fun y -> y > x ) xs in quicksort' small @ (x :: quicksort' large);; Zdefiniuj poprawną i efektywniejszą wersję (w której podział listy jest wykonywany w czasie liniowym w jednym przejściu) funkcji quicksort. 6. Zdefiniuj funkcje sortowania a) przez wstawianie z zachowaniem stabilności i złożoności O(n 2 ) insort : ( a-> a->bool) -> a list -> a list. b) przez łączenie (scalanie) z zachowaniem stabilności i złożoności O(n lg n) mergesort : ( a-> a->bool) -> a list -> a list. Pierwszy parametr jest funkcją, sprawdzającą porządek. Zamieść testy sprawdzające stabilność. Zdzisław Spławski Programowanie funkcyjne 26