Funkcjonały dla list

26  Download (0)

Pełen tekst

(1)

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

(2)

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

(3)

Funkcje jako dane

Funkcje są często przekazywane jako dane w obliczeniach numerycznych.

Poniższy funkcjonał oblicza sumę . 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 anonimowych. Możemy obliczyć np. bez konieczności oddzielnego definiowania funkcji podnoszenia do kwadratu.

sigma (function k -> float(k*k)) 10;;

- : float = 285.

= 1 0 ( )

m

i f i

9 2

k=0k

(4)

Funkcje jako dane

Wykorzystując ten funkcjonał łatwo obliczyć np. .

# 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ć to na , 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ść można teraz wyliczyć prościej:

# sigma2 (function (k,l) -> float(k+l)) 4 5;;

- : float = 70.

∑ ∑

3i=0 4j=0i+ j

∑ ∑

=

= 1 0

1

0 ( , )

m i

n

j g i j

∑ ∑

3i=0 4j=0i+ j

(5)

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

Rozwijanie i zwijanie funkcji

(6)

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'

Składanie funkcji

(7)

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]

(8)

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"]

(9)

# 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(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! *)

Filter z rekursją ogonową

(10)

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]

(11)

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]

(12)

# 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 fi 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>

Funkcjonały dla list – uogólnienie typowych funkcji

(13)

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.

(14)

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"

(15)

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"

Abstrakcja pomaga zauważyć i wyrazić analogie

(16)

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

Przykłady algebr abstrakcyjnych (homogenicznych)

(17)

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

(18)

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

(19)

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 | ifB then C else C fi | whileB 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.

(20)

Składnia i semantyka prostego języka imperatywnego PJI

Składnia abstrakcyjna

C ::= skip | C; C| V := E | ifB 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.

(21)

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.

Funkcjonał dla punktu stałego

(22)

# 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>

.

Funkcje semantyczne dla PJI

(23)

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

Semantyka denotacyjna silni

(24)

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.

(25)

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:=Eto 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)

(26)

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ść.

Obraz

Updating...

Cytaty

Powiązane tematy :