Programowanie funkcyjne/Procedury wyższych rzędów: Różnice pomiędzy wersjami
Nie podano opisu zmian |
|||
Linia 28: | Linia 28: | ||
==Czy istnieją procedury wieloargumentowe?== | ==Czy istnieją procedury wieloargumentowe?== | ||
Typy proceduralne to typy postaci <math>\mbox{argument} \to \mbox{wynik}</math>. Ponieważ zarówno argument, jak i wynik może być typu proceduralnego, więc typ może zawierać więcej "strzałek". Zastanówmy się co z procedurami wieloargumentowymi. Rozważmy następujące dwie definicje: | Typy proceduralne to typy postaci <math> \mbox{argument} \to \mbox{wynik}</math>. Ponieważ zarówno argument, jak i wynik może być typu proceduralnego, więc typ może zawierać więcej "strzałek". Zastanówmy się co z procedurami wieloargumentowymi. Rozważmy następujące dwie definicje: | ||
'''let''' plus (x, y) = x + y;; | '''let''' plus (x, y) = x + y;; | ||
Linia 57: | Linia 57: | ||
==Przykład: Sumy częściowe szeregów== | ==Przykład: Sumy częściowe szeregów== | ||
Przykład - szereg (kolejnych liczb naturalnych, kwadratów, <math>\frac{1}{(4i-3)(4i-1)}</math> zbieżny do <math>\frac{\pi}{8}</math>. Istota szeregu jako funkcja wyższego rzędu, sumowane elementy jako | Przykład - szereg (kolejnych liczb naturalnych, kwadratów, <math> \frac{1}{(4i-3)(4i-1)}</math> zbieżny do <math> \frac{\pi}{8}</math>. Istota szeregu jako funkcja wyższego rzędu, sumowane elementy jako | ||
funkcje liczb całkowitych. Procedura sumująca <math>n</math> pierwszych elementów danego szeregu (podać specyfikację <tt>sum</tt>): | funkcje liczb całkowitych. Procedura sumująca <math> n</math> pierwszych elementów danego szeregu (podać specyfikację <tt>sum</tt>): | ||
'''let''' '''rec''' szereg f n = | '''let''' '''rec''' szereg f n = | ||
Linia 89: | Linia 89: | ||
[[grafika:pf-rys-4-1.gif||center]] | [[grafika:pf-rys-4-1.gif||center]] | ||
Zauważmy, że jeżeli naszym przybliżeniem zera jest <math>x</math>, to styczna do wykresu funkcji przecina oś X w punkcie | Zauważmy, że jeżeli naszym przybliżeniem zera jest <math> x</math>, to styczna do wykresu funkcji przecina oś X w punkcie | ||
<math>x - \frac{(f\ x)}{(f'\ x)}</math>. | <math> x - \frac{(f\ x)}{(f'\ x)}</math>. | ||
Jest to przykład algorytmu polegającego na iteracyjnym przybliżaniu wyniku. Spróbujmy sformułować najpierw ogólny schemat takiego | Jest to przykład algorytmu polegającego na iteracyjnym przybliżaniu wyniku. Spróbujmy sformułować najpierw ogólny schemat takiego | ||
Linia 101: | Linia 101: | ||
iteruj (popraw poczatek) popraw czy_dobre wynik;; | iteruj (popraw poczatek) popraw czy_dobre wynik;; | ||
Zastosujmy teraz ten schemat do zaimplementowania metody stycznych Newtona. Zauważmy, że jeżeli naszym przybliżeniem zera jest <math>x</math>, to styczna do wykresu funkcji przecina oś X w punkcie <math>x - \frac{(f\ x)}{(f'\ x)}</math>. | Zastosujmy teraz ten schemat do zaimplementowania metody stycznych Newtona. Zauważmy, że jeżeli naszym przybliżeniem zera jest <math> x</math>, to styczna do wykresu funkcji przecina oś X w punkcie <math> x - \frac{(f\ x)}{(f'\ x)}</math>. | ||
'''let''' id x = x;; | '''let''' id x = x;; | ||
Linia 112: | Linia 112: | ||
iteruj x popraw czy_dobre id;; | iteruj x popraw czy_dobre id;; | ||
Zwróćmy uwagę na to, że procedura <tt>popraw-newton</tt> jest lokalna i ma dostęp do funkcji <tt>f</tt>. Pozostało nam jeszcze napisanie procedury <tt>pochodna</tt>. Metoda Newtona liczenia pierwiastków kwadratowych to szczególny przypadek metody Newtona znajdowania zer, dla funkcji <math>f(x) = x^2 - a</math>. Funkcja ta ma zera w punktach <math>\pm \sqrt{a}</math>. Ponadto <math>f'(x) = 2x</math>, czyli nasz algorytm przekształca przybliżenie <math>x</math> w | Zwróćmy uwagę na to, że procedura <tt>popraw-newton</tt> jest lokalna i ma dostęp do funkcji <tt>f</tt>. Pozostało nam jeszcze napisanie procedury <tt>pochodna</tt>. Metoda Newtona liczenia pierwiastków kwadratowych to szczególny przypadek metody Newtona znajdowania zer, dla funkcji <math> f(x) = x^2 - a</math>. Funkcja ta ma zera w punktach <math> \pm \sqrt{a}</math>. Ponadto <math> f'(x) = 2x</math>, czyli nasz algorytm przekształca przybliżenie <math> x</math> w | ||
<math> | <math> | ||
x - \frac{f(x)}{f'(x)} = | x - \frac{f(x)}{f'(x)} = | ||
x - \frac{x^2 - a}{2x} = | x - \frac{x^2 - a}{2x} = | ||
Linia 122: | Linia 122: | ||
</math> | </math> | ||
Zaczynając w punkcie 1 przybliżamy <math>\sqrt{a}</math>. | Zaczynając w punkcie 1 przybliżamy <math> \sqrt{a}</math>. | ||
'''let''' sqrt a = | '''let''' sqrt a = | ||
Linia 130: | Linia 130: | ||
==Punkty stałe funkcji== | ==Punkty stałe funkcji== | ||
Punkt stały funkcji <math>f</math>, to taki <math>x</math>, że <math>f(x) = x</math>. W przypadku niektórych funkcji (i określonych <math>x</math>-ów) ciąg <math>x, f(x), f^2(x), f^3(x), \dots</math> jest zbieżny do pewnego punktu stałego <math>f</math>. Możemy zaimplementować tę metodę: | Punkt stały funkcji <math> f</math>, to taki <math> x</math>, że <math> f(x) = x</math>. W przypadku niektórych funkcji (i określonych <math> x</math>-ów) ciąg <math> x, f(x), f^2(x), f^3(x), \dots</math> jest zbieżny do pewnego punktu stałego <math> f</math>. Możemy zaimplementować tę metodę: | ||
'''let''' punkt_staly f x = | '''let''' punkt_staly f x = | ||
Linia 137: | Linia 137: | ||
iteruj x f blisko f;; | iteruj x f blisko f;; | ||
Przykładową funkcją, której punkt stały można znaleźć tą metodą jest <math>\cos(x)</math>. | Przykładową funkcją, której punkt stały można znaleźć tą metodą jest <math> \cos(x)</math>. | ||
punkt_staly cos 1.0;; | punkt_staly cos 1.0;; | ||
''0.73908\dots'' | ''0.73908\dots'' | ||
Proszę to sprawdzić na jakimś nudnym wykładzie --- wystarczy ciągle stukać w klawisz <math>\cos</math>. | Proszę to sprawdzić na jakimś nudnym wykładzie --- wystarczy ciągle stukać w klawisz <math> \cos</math>. | ||
Obliczanie punktów stałych moglibyśmy zastosować do obliczania pierwiastków --- punkt stały funkcji <math>y \to \frac{x}{y}</math> to <math>\sqrt{x}</math>. Jednak obliczenie: | Obliczanie punktów stałych moglibyśmy zastosować do obliczania pierwiastków --- punkt stały funkcji <math> y \to \frac{x}{y}</math> to <math> \sqrt{x}</math>. Jednak obliczenie: | ||
punkt_staly (function y -> x /. y) 1.0;; | punkt_staly (function y -> x /. y) 1.0;; | ||
nie jest zbieżne: | nie jest zbieżne: | ||
<math> | <math> | ||
1 \to \frac{x}{1} = x \to \frac{x}{x} = 1 \to \dots | 1 \to \frac{x}{1} = x \to \frac{x}{x} = 1 \to \dots | ||
</math> | </math> |
Wersja z 08:51, 27 lip 2006
Wstęp
Jak wcześniej zaznaczyliśmy, procedury są równie dobrymi wartościami jak wszystkie inne. W szczególności procedury mogą być argumentami procedur lub ich wynikami. Dzisiaj zajmiemy się procedurami wyższych rzędów, tzn. procedurami przetwarzającymi procedury.
Kilka prostych przykładów
Podniesienie funkcji do kwadratu
let twice f = function x -> f (f x);; val twice : ('a -> 'a) -> 'a -> 'a = <fun> twice (function x -> x * (x+1)) 2;; - : int = 42
Zwróćmy (pobieżnie) uwagę na typ procedury twice. (Krotka zapowiedź typów proceduralnych. Bez zagłębiania się w polimorfizm.) Jest to procedura, której parametrem jest procedura, a wynikiem jest procedura takiego samego typu, jak argument.
Składanie funkcji
let compose f g = function x -> f (g x);; val compose : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = <fun> let twice f = compose f f;;
Iterowanie funkcji:
let id x = x;; let rec iterate n f = if n = 0 then id else compose (iterate (n-1) f) f;;
Czy istnieją procedury wieloargumentowe?
Typy proceduralne to typy postaci . Ponieważ zarówno argument, jak i wynik może być typu proceduralnego, więc typ może zawierać więcej "strzałek". Zastanówmy się co z procedurami wieloargumentowymi. Rozważmy następujące dwie definicje:
let plus (x, y) = x + y;; val plus : int * int -> int = <fun> let plus x y = x + y;; val plus : int -> int -> int = <fun>
Pierwsza procedura bierze parę wartości i oblicza wynik. Druga procedura bierze pierwszy argument i jej wynikiem jest procedura, która gdy dostanie drugi argument, to obliczy sumę. Inaczej mówiąc, bierze argumenty na raty.
Ta druga postać jest wygodniejsza, ze względu na możliwość podania tylko części argumentów.
let plus x y = x + y;; let inc = plus 1;;
Obie postacie, są równoważne. Dowodem na to są poniższe dwie procedury przekształcające jedną w postać w drugą i odwrotnie. Jakiego typu są te procedury?
let curry f = function x -> function y -> f (x, y);; let uncurry f = function (x, y) -> f x y;;
Standardowa postać podawania argumentów procedury jest curry. Tak więc przedstawione przykłady można zdefiniować i tak:
let twice f x = f (f x);; let compose f g x = f (g x);; let curry f x y = f (x, y);; let uncurry f (x, y) = f x y;;
Przykład: Sumy częściowe szeregów
Przykład - szereg (kolejnych liczb naturalnych, kwadratów, zbieżny do . Istota szeregu jako funkcja wyższego rzędu, sumowane elementy jako funkcje liczb całkowitych. Procedura sumująca pierwszych elementów danego szeregu (podać specyfikację sum):
let rec szereg f n = if n = 0 then 0.0 else f n + szereg f (n-1);; let szereg_pi_8 n = szereg (function i -> 1. /. ((4. *. float i -. 3.) *. (4. *. float i -. 1.))) n;; let pi = 8. *. szereg_pi_8 1000;;
Procedura szereg może służyć do obliczania sumczęściowych dowolnych szeregów.
Przykład: Różniczkowanie funkcji
Zdefiniujmy najpierw różniczkę:
let rozniczka f x dx = (f (x +. dx) -. f x) /. dx;;
Wówczas pochodną możemy przybliżyć następująco:
let pochodna f x = rozniczka f x epsilon;;
Zwróćmy uwagę, że pochodna jest procedurą jednoargumentową i wynikiem (pochodna f) jest funkcja będąca pochodną f.
Metoda Newtona
Zakładamy, że dana funkcja jest różniczkowalna. Pomijamy kwestie stopu i dzielenia przez 0. Metoda Newtona, nazywana też metodą stycznych, polega na przybliżaniu zera funkcji w następujący sposób: w punkcie będącym aktualnym przybliżeniem rysujemy styczną do wykresu funkcji i kolejnym przybliżeniem jest punkt przecięcia stycznej z osią X.

Zauważmy, że jeżeli naszym przybliżeniem zera jest , to styczna do wykresu funkcji przecina oś X w punkcie .
Jest to przykład algorytmu polegającego na iteracyjnym przybliżaniu wyniku. Spróbujmy sformułować najpierw ogólny schemat takiego postępowania iteracyjnego:
let rec iteruj poczatek popraw czy_dobre wynik = if czy_dobre poczatek then wynik poczatek else iteruj (popraw poczatek) popraw czy_dobre wynik;;
Zastosujmy teraz ten schemat do zaimplementowania metody stycznych Newtona. Zauważmy, że jeżeli naszym przybliżeniem zera jest , to styczna do wykresu funkcji przecina oś X w punkcie .
let id x = x;; let newton f x = let p = pochodna f in let czy_dobre x = abs (f x) < epsilon and popraw x = x -. (f x) /. (p x) in iteruj x popraw czy_dobre id;;
Zwróćmy uwagę na to, że procedura popraw-newton jest lokalna i ma dostęp do funkcji f. Pozostało nam jeszcze napisanie procedury pochodna. Metoda Newtona liczenia pierwiastków kwadratowych to szczególny przypadek metody Newtona znajdowania zer, dla funkcji . Funkcja ta ma zera w punktach . Ponadto , czyli nasz algorytm przekształca przybliżenie w
Zaczynając w punkcie 1 przybliżamy .
let sqrt a = let f x = a -. square x in newton f 1.;;
Punkty stałe funkcji
Punkt stały funkcji , to taki , że . W przypadku niektórych funkcji (i określonych -ów) ciąg jest zbieżny do pewnego punktu stałego . Możemy zaimplementować tę metodę:
let punkt_staly f x = let blisko x = abs (x -. f x) < epsilon in iteruj x f blisko f;;
Przykładową funkcją, której punkt stały można znaleźć tą metodą jest .
punkt_staly cos 1.0;; 0.73908\dots
Proszę to sprawdzić na jakimś nudnym wykładzie --- wystarczy ciągle stukać w klawisz .
Obliczanie punktów stałych moglibyśmy zastosować do obliczania pierwiastków --- punkt stały funkcji to . Jednak obliczenie:
punkt_staly (function y -> x /. y) 1.0;;
nie jest zbieżne:
W takich przypadkach czasami pomaga "wytłumienie" oscylacji takiego ciągu przez jego uśrednienie.
let usrednienie f = function x -> average x (f x);; let sqrt x = punkt_staly (usrednienie (function y -> x /. y)) 1.0;;
W ten sposób uzyskujemy po raz kolejny metodę pierwiastkowania Newtona.
Zastosowania procedur wyższych rzędów
Procedury wyższych rzędów to jeden z tych elementów języka, który jest czysto funkcyjny. W językach imperatywnych procedury mogą być parametrami procedur, lecz nie wynikami. W przypadku języków funkcyjnych mamy pełną swobodę.
Zastosowania procedur wyższych rzędów można podzielić na dwie grupy:
- Pewne pojęcia matematyczne, zwłaszcza te dotyczące funkcji, w naturalny sposób przekładają się na procedury wyższych rzędów, np.: sumy częściowe szeregów, składanie funkcji, różniczkowanie funkcji itp.
- Procedury są jeszcze jednym typem danych. Przyjmując takie podejście, procedury przetwarzające takie dane będą procedurami wyższych rzędów.
- Procedury wyższych rzędów są również narzędziem abstrakcji. Jeżeli ten sam fragment kodu pojawia się w kilku miejscach, to naturalnym jest wyłonienie go w postaci (zwykłej) procedury.
- Jeżeli ten sam schemat kodu pojawia się w wielu miejscach, ale różni się wypełniającymi go fragmentami, to schemat ten możemy ująć w postaci procedury wyższego rzędu, a fragmenty do wypełnienia staną się parametrami proceduralnymi. Przykładem może być tu procedura iteruj.
Procedury wyższych rzędów i listy
Funkcje przetwarzające listy często mają ten sam schemat. Spróbujmy ująć ten schemat w postaci procedur wyższych rzędów. Najczęściej przeglądamy kolejne elementy listy i obliczamy na ich podstawie pewien wynik. W schemacie tym mamy następujące elementy zmienne:
- elementy listy przetwarzamy od lewej do prawej, lub odwrotnie,
- wynik dla pustej listy,
- wynik dla nie pustej listy, jako funkcja głowy i wyniku obliczonego dla ogona.
W zależności od kierunku przetwarzania listy, schemat ten możemy ująć w postaci następujących dwóch procedur:
let rec foldr f l a = match l with [] -> a | h::t -> f h (foldr f t a);; let rec foldl f a l = match l with [] -> a | h::t -> foldl f (f a h) t;; let fold = foldl;;
Oto kilka prostych zastosowań:
let sum l = fold (+) 0 l;; let prod l = fold ( * ) 1 l;;
Inny schemat, to przetwarzanie wszystkich elementów listy, element po elemencie.
let map f l = foldr (fun h t -> (f h)::t) l [];;
Kolejny częsty schemat, to selekcja elementów listy --- sprawdzamy pewien warunek i zostawiamy tylko elementy spełniające go.
let filter p l = foldr (fun h t -> if p h then h::t else t) l [];;