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

Podobne dokumenty