list w porządku
Transkrypt
list w porządku
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 −1 Poniższy funkcjonał oblicza sumę ∑i =0 f (i) . Dla efektywności wykorzystuje on 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 2 9 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 Wykorzystując ten funkcjonał łatwo obliczyć np. ∑3i =0 ∑4j =0 i + j . # 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 −1 n −1 to na ∑i =0 ∑ j =0 g (i, j ) , czyli # 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 =0 j =0 i+ 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 [x1; ... ; xn]-> [f x1; ... ; f xn] # 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 let rec find [] -> acc | x :: xs -> in find [];; val filter_bad p = acc = function if p x then find (acc@[x]) xs else find acc xs : ('a -> bool) -> 'a list -> 'a list = <fun> (* Złożoność O(n2). 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 [x1; ... ; xn]-> f(...(f(f acc x1)x2)...)xn # 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 [x1; ... ; xn] acc -> f x1(f x2(...(f xn 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¬ σ = σ «C1; C2 ¬ σ ' «C2 ¬ («C1¬ σ) 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 Rekonstrukcja typu # let f z y x = y (y z x);; 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;; c) let f3 x y z = x y z;; b) let f2 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(n2) 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