Programowanie funkcyjne/Wyliczanie procesów: Różnice pomiędzy wersjami
Linia 5: | Linia 5: | ||
==Model obliczeń== | ==Model obliczeń== | ||
<tt>let</tt>(globalny) tworzy nową ramkę zawierającą definiowaną wartość(-ci). | |||
Dotychczasowe środowisko staje się środowiskiem otaczającym tej ramki. | <tt>let</tt> (globalny) tworzy nową ramkę zawierającą definiowaną wartość(-ci). Dotychczasowe środowisko staje się środowiskiem otaczającym tej ramki. Utworzona ramka staje się aktualnym środowiskiem (globalnym). | ||
Utworzona ramka staje się aktualnym środowiskiem (globalnym). | |||
TU GRAFIKA 1 | |||
Ramka może zawierać więcej niż jedną definicję --- <tt>and</tt>. | |||
TU GRAFIKA 2 | |||
Ramka może zawierać więcej niż jedną definicję --- <tt>and</tt>. | |||
===Procedura jako wartość=== | ===Procedura jako wartość=== | ||
Na wartość procedury składają sie trzy elementy: | Na wartość procedury składają sie trzy elementy: | ||
nazwy argumentów% | nazwy argumentów% | ||
\footnote{% | \footnote{% | ||
Formalnie rzecz biorąc, jest to jeden argument. | Formalnie rzecz biorąc, jest to jeden argument. Dopóki jednak nie wprowadzimy procedur wyższych rzędów, oraz w sytuacjach, gdy takie uproszczenie nie będzie prowadzić do nieporozumień, będziemy dopuszczać obiekty proceduralne z wieloma argumentami. }% | ||
Dopóki jednak nie wprowadzimy procedur wyższych rzędów, | , treść procedury i wskaźnik do ramki reprezentującej środowisko, w którym zdefiniowano procedurę. Pierwsze dwa elementy przekładają się, w trakcie kompilacji, na kod procedury. Natomiast wskaźnik do środowiska, w którym zdefiniowano procedurę może być ustalany w trakcie obliczeń --- dotyczy to np. procedur lokalnych. Reasumując, wartość procedury jest reprezentowana jako para wskaźników: do kodu skompilowanej procedury i do środowiska, w którym procedura została zdefiniowana. W przypadku procedur rekurencyjnych "środowisko, w którym zdefiniowano procedurę" zawiera jej własną definicję. | ||
oraz w sytuacjach, gdy takie uproszczenie nie będzie | |||
prowadzić do nieporozumień, będziemy dopuszczać | |||
obiekty proceduralne z wieloma argumentami. | |||
}% | |||
, treść procedury i wskaźnik do ramki | |||
reprezentującej środowisko, w którym zdefiniowano | |||
procedurę. | |||
Pierwsze dwa elementy przekładają się, w trakcie kompilacji, | |||
na kod procedury. | |||
Natomiast wskaźnik do środowiska, w którym zdefiniowano | |||
procedurę może być ustalany w trakcie obliczeń --- | |||
dotyczy to np. | |||
wskaźników: do kodu skompilowanej procedury i do | |||
środowiska, w którym procedura została zdefiniowana. | |||
W przypadku procedur rekurencyjnych | |||
zdefiniowano procedurę | |||
===Wartości różnych typów danych=== | ===Wartości różnych typów danych=== | ||
Wartości elementarnych typów wbudowanych (takie jak | |||
Wartości elementarnych typów wbudowanych (takie jak <tt>int</tt>, czy <tt>bool</tt>) są pamiętane wprost. Wartości typów złożonych są pamiętane jako wskaźniki do odpowiednich wartości. Dzięki temu wynikiem obliczenia wartości jest zawsze albo wskaźnik, albo wartość, która zajmuje porównywalnie dużo miejsca w pamięci do wskaźnika. W rezultacie definiując stałe o złożonych wartościach nie kopiujemy tych wartości, a jedynie wskaźniki do nich. | |||
Wartości typów złożonych są pamiętane jako wskaźniki do odpowiednich | |||
wartości. | |||
Dzięki temu wynikiem obliczenia wartości jest zawsze albo wskaźnik, | |||
albo wartość, która zajmuje porównywalnie dużo miejsca w pamięci | |||
do wskaźnika. | |||
W rezultacie definiując stałe o złożonych wartościach nie kopiujemy | |||
tych wartości, a jedynie wskaźniki do nich. | |||
Wartości różnych złożonych typów danych są generalnie tworzone jako | Wartości różnych złożonych typów danych są generalnie tworzone jako rekordy złożone ze wskaźników do wartości składowych: | ||
rekordy złożone ze wskaźników do wartości składowych: | * Element produktu kartezjańskiego jest reprezentowany jako n-tka wskaźników do wartości tworzących go. | ||
* | * Rekord --- podobnie do produktu kartezjańskiego. | ||
* Lista to albo pusta lista, albo para: głowa i ogon. | |||
* | * Warianty są pamiętane jako stała wyznaczająca operator i wskaźnik(i) do argumentu(-ów). | ||
* | |||
* | |||
===Aplikacja procedury do argumentów=== | ===Aplikacja procedury do argumentów=== | ||
Obliczenie wartości procedury polega na: | Obliczenie wartości procedury polega na: | ||
* | * wyliczeniu wszystkich elementów: procedury i jej argumentów, | ||
* | * stworzeniu nowej ramki, dla której środowiskiem otaczającym jest środowisko, w którym zdefiniowano procedurę, oraz nazwom parametrów formalnych przyporządkowano wartości argumentów, | ||
* wyliczeniu w takim środowisku wartości wyrażenia stanowiącego treść. | |||
* | |||
Przykład: | |||
Następujące definicje: | Następujące definicje: | ||
'''let''' x = 2;; | '''let''' x = 2;; | ||
'''let''' square x = x * x;; | '''let''' square x = x * x;; | ||
'''let''' pitagoras x y = square x + square y;; | '''let''' pitagoras x y = square x + square y;; | ||
TU GRAFIKA 3 | |||
Obliczenie wyrażenia: | Obliczenie wyrażenia: | ||
pitagoras x (x + 1) + pitagoras x ((square x) + 1);; | pitagoras x (x + 1) + pitagoras x ((square x) + 1);; | ||
będzie przebiegać następująco: | będzie przebiegać następująco: | ||
TU GRAFIKA 4 | |||
Przykład: | |||
Prześledźmy obliczenie prostego wyrażenia zawierającego | Prześledźmy obliczenie prostego wyrażenia zawierającego | ||
<math>\lambda</math>-abstrakcję: | <math>\lambda</math>-abstrakcję: | ||
TU GRAFIKA 5 | |||
Przykład: | |||
Wyliczanie procedur rekurencyjnych: | Wyliczanie procedur rekurencyjnych: | ||
let '''rec''' silnia n = | let '''rec''' silnia n = | ||
if n < 2 then 1 '''else''' n * silnia (n - 1);; | '''if''' n < 2 '''then''' 1 '''else''' n * silnia (n - 1);; | ||
silnia 3;; | silnia 3;; | ||
TU GRAFIKA 6 | |||
===Rekurencja ogonowa=== | ===Rekurencja ogonowa=== | ||
Jeśli wartości zwracane przez wszystkie wywołania rekurencyjne, bez żadnego przetwarzania są przekazywane jako wynik danej procedury, to mówimy o rekurencji ogonowej. W przypadku rekurencji ogonowej, w momencie wywołania rekurencyjnego nie musimy tworzyć nowej ramki --- możemy wykorzystać istniejącą ramkę zmieniając jedynie wartości argumentów. | |||
Przykład: | |||
Silnia z akumulatorem: | Silnia z akumulatorem: | ||
let '''rec''' s a x = | '''let''' '''rec''' s a x = | ||
if x <= 1 then a '''else''' s (a * x) (x - 1);; | '''if''' x <= 1 '''then''' a '''else''' s (a * x) (x - 1);; | ||
'''let''' silnia x = s 1 x;; | '''let''' silnia x = s 1 x;; | ||
silnia 3;; | silnia 3;; | ||
TU GRAFIKA 6 | |||
Przykład: | |||
Liczby Fibonacciego z akumulatorem: | Liczby Fibonacciego z akumulatorem: | ||
let '''rec''' fibpom a b n = | '''let''' '''rec''' fibpom a b n = | ||
if n = 0 then a '''else''' fibpom b (a + b) (n - 1);; | '''if''' n = 0 '''then''' a '''else''' fibpom b (a + b) (n - 1);; | ||
'''let''' fib n = fibpom 0 1 n;; | '''let''' fib n = fibpom 0 1 n;; | ||
fib 5;; | fib 5;; | ||
TU GRAFIKA 8 | |||
===Definicje lokalne=== | ===Definicje lokalne=== | ||
Ewaluacja <tt>let-in</tt> jest analogiczna do rozwinięcia tej formy specjalnej do zastosowania <math>\lambda</math>-abstrakcji. | Ewaluacja <tt>let-in</tt> jest analogiczna do rozwinięcia tej formy specjalnej do zastosowania <math>\lambda</math>-abstrakcji. | ||
Wyrażenie postaci: | Wyrażenie postaci: | ||
let <math>a</math> = <math>b</math> '''in''' <math>c</math> | '''let''' <math>a</math> = <math>b</math> '''in''' <math>c</math> | ||
jest równoważne: | jest równoważne: | ||
(function <math>a</math> -> <math>c</math>) <math>b</math> | ('''function''' <math>a</math> -> <math>c</math>) <math>b</math> | ||
Schemat obliczenia wyrażenia <tt>let</tt> jest następujący:* | Schemat obliczenia wyrażenia <tt>let</tt> jest następujący: | ||
* tworzona jest ramka zawierająca definicję symboli lokalnych (otaczającym ją środowiskiem jest aktualne środowisko), | |||
* | * w tak powstałym środowisku wyliczana jest wartość wyrażenia, | ||
* | * po czym aktualnym środowiskiem jest to, co poprzednio. | ||
Przykład: | |||
Następujący przykład pokazuje sposób realizacji definicji | Następujący przykład pokazuje sposób realizacji definicji | ||
lokalnych. | lokalnych. | ||
'''let''' f x = | '''let''' f x = | ||
let y = x + 2 | '''let''' y = x + 2 | ||
in | '''in''' | ||
let z = 2 * y | '''let''' z = 2 * y | ||
in | '''in''' | ||
x + y + z;; | x + y + z;; | ||
f 9;; | f 9;; | ||
TU GRAFIKA 9 | |||
Przykład: | |||
Niniejszy przykład ilustruje mniej typową kombinację | Niniejszy przykład ilustruje mniej typową kombinację użycia definicji lokalnych i procedur. | ||
użycia definicji lokalnych i procedur. | |||
'''let''' f = | '''let''' f = | ||
let g x = x - 8 | '''let''' g x = x - 8 | ||
in | '''in''' | ||
fun x -> g (2 * x);; | '''fun''' x -> g (2 * x);; | ||
'''let''' h x = f (x * x);; | '''let''' h x = f (x * x);; | ||
h 5;; | h 5;; | ||
Zwróćmy uwagę na procedurę pomocniczą <tt>g</tt>.Procedura ta jest widoczna tylko wewnątrz procedury | Zwróćmy uwagę na procedurę pomocniczą <tt>g</tt>.Procedura ta jest widoczna tylko wewnątrz procedury <tt>f</tt>, a jednak jej definicja ma tylko jedną instancję. | ||
<tt>f</tt>, a jednak jej definicja ma tylko | |||
TU GRAFIKA 10 | |||
Przykład: | |||
'''let''' f x y = | '''let''' f x y = | ||
let | '''let''' | ||
g a = a * x * y | g a = a * x * y | ||
in | '''in''' | ||
g x + g y;; | g x + g y;; | ||
f 4 2 - f 2 1;; | f 4 2 - f 2 1;; | ||
W tym przykładzie, w każdym wywołaniu procedury <tt>f</tt>powstaje osobna instancja procedury lokalnej <tt>g</tt>.Jest to naturalne zważywszy, że procedura <tt>g</tt> korzysta z argumentów wywołania procedury <tt>f</tt>.\begin{center} | W tym przykładzie, w każdym wywołaniu procedury <tt>f</tt>powstaje osobna instancja procedury lokalnej <tt>g</tt>.Jest to naturalne zważywszy, że procedura <tt>g</tt> korzysta z argumentów wywołania procedury <tt>f</tt>.\begin{center} | ||
TU GRAFIKA 11 | |||
===Odśmiecanie=== | ===Odśmiecanie=== | ||
Ramki nie tworzą listy, ale drzewo (biorąc pod uwagę wskaźniki na | Ramka zawierająca lokalne definicje nie musi stawać się od razu zbędna, gdyż mogą na nią wskazywać wartości proceduralne. Zbędne ramki są usuwane w miarę potrzeb --- zbędne to takie, na które nic nie wskazuje lub, do których nie można dotrzeć z aktualnego środowiska poprzez wskaźniki. | ||
otaczające środowisko). | |||
Ramki, które trzeba trzymać w pamięci też nie muszą tworzyć | Ramki nie tworzą listy, ale drzewo (biorąc pod uwagę wskaźniki na otaczające środowisko). Ramki, które trzeba trzymać w pamięci też nie muszą tworzyć listy, tylko drzewo. Występuje to np. wówczas, gdy wynikiem procedury jest procedura. Zahaczamy tu o procedury wyższych rzędów, ale nie będziemy tego tematu teraz rozwijać. | ||
listy, tylko drzewo. | |||
Występuje to np. | Przykład: | ||
procedura. | Niniejszy przykład pokazuje sytuację, gdy ramki powstałe w czasie wywołania procedury mogą być potrzebne po jego zakończeniu. | ||
Zahaczamy tu o procedury wyższych rzędów, ale nie będziemy | |||
tego tematu teraz rozwijać. | |||
Niniejszy przykład pokazuje sytuację, gdy ramki powstałe w | |||
czasie wywołania procedury mogą być potrzebne po jego | |||
zakończeniu. | |||
'''let''' f x = | '''let''' f x = | ||
let g y = x * y | '''let''' g y = x * y | ||
in g;; | '''in''' g;; | ||
'''let''' h = f 6;; | '''let''' h = f 6;; | ||
h 7;; | h 7;; | ||
TU GRAFIKA 12 | |||
Ramka zawierająca definicję procedury <tt>g</tt> powstaje w wyniku wywołania <tt>f 6</tt>, ale jest potrzebna później, do obliczenia <tt>h 7</tt>.\end{przyklad} | Ramka zawierająca definicję procedury <tt>g</tt> powstaje w wyniku wywołania <tt>f 6</tt>, ale jest potrzebna później, do obliczenia <tt>h 7</tt>.\end{przyklad} | ||
Przykład: | |||
Poniższy przykład ilustruje sposób przekazywania wielu | Poniższy przykład ilustruje sposób przekazywania wielu argumentów, tak jak to jest realizowane --- po jednym na raz. | ||
argumentów, tak jak to jest realizowane --- po jednym na | |||
raz. | |||
'''let''' g x = | '''let''' g x = | ||
let | '''let''' | ||
a = x * x | a = x * x | ||
in | '''in''' | ||
fun y -> y + a;; | '''fun''' y -> y + a;; | ||
'''let''' h x = (g x) x;; | '''let''' h x = (g x) x;; | ||
h 6;; | h 6;; | ||
TU GRAFIKA 13 | |||
Wersja z 13:28, 18 lip 2006
Środowisko i ramki.
Podział środowiska na ciąg ramek połączonych wskaźnikami w listę jednokierunkową. Poszukując w środowisku wartości stałej przeglądamy kolejne ramki. Definiowanie nowych wartości powoduje dodanie nowej ramki zawierającej wprowadzane definicje. Można przesłaniać wartości już istniejące w słowniku. Nie powoduje to jednak ich usunięcia ze słownika.
Każda ramka reprezentuje pewne środowisko. Aktualne środowisko jest reprezentowane przez ostatnio dodaną ramkę. Środowisko, do którego prowadzi wskaźnik od danej ramki nazywamy środowiskiem otaczającym.
Model obliczeń
let (globalny) tworzy nową ramkę zawierającą definiowaną wartość(-ci). Dotychczasowe środowisko staje się środowiskiem otaczającym tej ramki. Utworzona ramka staje się aktualnym środowiskiem (globalnym).
TU GRAFIKA 1
Ramka może zawierać więcej niż jedną definicję --- and.
TU GRAFIKA 2
Procedura jako wartość
Na wartość procedury składają sie trzy elementy: nazwy argumentów% \footnote{% Formalnie rzecz biorąc, jest to jeden argument. Dopóki jednak nie wprowadzimy procedur wyższych rzędów, oraz w sytuacjach, gdy takie uproszczenie nie będzie prowadzić do nieporozumień, będziemy dopuszczać obiekty proceduralne z wieloma argumentami. }% , treść procedury i wskaźnik do ramki reprezentującej środowisko, w którym zdefiniowano procedurę. Pierwsze dwa elementy przekładają się, w trakcie kompilacji, na kod procedury. Natomiast wskaźnik do środowiska, w którym zdefiniowano procedurę może być ustalany w trakcie obliczeń --- dotyczy to np. procedur lokalnych. Reasumując, wartość procedury jest reprezentowana jako para wskaźników: do kodu skompilowanej procedury i do środowiska, w którym procedura została zdefiniowana. W przypadku procedur rekurencyjnych "środowisko, w którym zdefiniowano procedurę" zawiera jej własną definicję.
Wartości różnych typów danych
Wartości elementarnych typów wbudowanych (takie jak int, czy bool) są pamiętane wprost. Wartości typów złożonych są pamiętane jako wskaźniki do odpowiednich wartości. Dzięki temu wynikiem obliczenia wartości jest zawsze albo wskaźnik, albo wartość, która zajmuje porównywalnie dużo miejsca w pamięci do wskaźnika. W rezultacie definiując stałe o złożonych wartościach nie kopiujemy tych wartości, a jedynie wskaźniki do nich.
Wartości różnych złożonych typów danych są generalnie tworzone jako rekordy złożone ze wskaźników do wartości składowych:
- Element produktu kartezjańskiego jest reprezentowany jako n-tka wskaźników do wartości tworzących go.
- Rekord --- podobnie do produktu kartezjańskiego.
- Lista to albo pusta lista, albo para: głowa i ogon.
- Warianty są pamiętane jako stała wyznaczająca operator i wskaźnik(i) do argumentu(-ów).
Aplikacja procedury do argumentów
Obliczenie wartości procedury polega na:
- wyliczeniu wszystkich elementów: procedury i jej argumentów,
- stworzeniu nowej ramki, dla której środowiskiem otaczającym jest środowisko, w którym zdefiniowano procedurę, oraz nazwom parametrów formalnych przyporządkowano wartości argumentów,
- wyliczeniu w takim środowisku wartości wyrażenia stanowiącego treść.
Przykład: Następujące definicje:
let x = 2;; let square x = x * x;; let pitagoras x y = square x + square y;;
TU GRAFIKA 3
Obliczenie wyrażenia:
pitagoras x (x + 1) + pitagoras x ((square x) + 1);;
będzie przebiegać następująco:
TU GRAFIKA 4
Przykład:
Prześledźmy obliczenie prostego wyrażenia zawierającego -abstrakcję:
TU GRAFIKA 5
Przykład: Wyliczanie procedur rekurencyjnych:
let rec silnia n = if n < 2 then 1 else n * silnia (n - 1);; silnia 3;;
TU GRAFIKA 6
Rekurencja ogonowa
Jeśli wartości zwracane przez wszystkie wywołania rekurencyjne, bez żadnego przetwarzania są przekazywane jako wynik danej procedury, to mówimy o rekurencji ogonowej. W przypadku rekurencji ogonowej, w momencie wywołania rekurencyjnego nie musimy tworzyć nowej ramki --- możemy wykorzystać istniejącą ramkę zmieniając jedynie wartości argumentów.
Przykład: Silnia z akumulatorem:
let rec s a x = if x <= 1 then a else s (a * x) (x - 1);; let silnia x = s 1 x;; silnia 3;;
TU GRAFIKA 6
Przykład: Liczby Fibonacciego z akumulatorem:
let rec fibpom a b n = if n = 0 then a else fibpom b (a + b) (n - 1);; let fib n = fibpom 0 1 n;; fib 5;;
TU GRAFIKA 8
Definicje lokalne
Ewaluacja let-in jest analogiczna do rozwinięcia tej formy specjalnej do zastosowania -abstrakcji. Wyrażenie postaci:
let = in
jest równoważne:
(function -> )
Schemat obliczenia wyrażenia let jest następujący:
- tworzona jest ramka zawierająca definicję symboli lokalnych (otaczającym ją środowiskiem jest aktualne środowisko),
- w tak powstałym środowisku wyliczana jest wartość wyrażenia,
- po czym aktualnym środowiskiem jest to, co poprzednio.
Przykład: Następujący przykład pokazuje sposób realizacji definicji lokalnych.
let f x = let y = x + 2 in let z = 2 * y in x + y + z;; f 9;;
TU GRAFIKA 9
Przykład: Niniejszy przykład ilustruje mniej typową kombinację użycia definicji lokalnych i procedur.
let f = let g x = x - 8 in fun x -> g (2 * x);; let h x = f (x * x);; h 5;;
Zwróćmy uwagę na procedurę pomocniczą g.Procedura ta jest widoczna tylko wewnątrz procedury f, a jednak jej definicja ma tylko jedną instancję.
TU GRAFIKA 10
Przykład:
let f x y = let g a = a * x * y in g x + g y;; f 4 2 - f 2 1;;
W tym przykładzie, w każdym wywołaniu procedury fpowstaje osobna instancja procedury lokalnej g.Jest to naturalne zważywszy, że procedura g korzysta z argumentów wywołania procedury f.\begin{center}
TU GRAFIKA 11
Odśmiecanie
Ramka zawierająca lokalne definicje nie musi stawać się od razu zbędna, gdyż mogą na nią wskazywać wartości proceduralne. Zbędne ramki są usuwane w miarę potrzeb --- zbędne to takie, na które nic nie wskazuje lub, do których nie można dotrzeć z aktualnego środowiska poprzez wskaźniki.
Ramki nie tworzą listy, ale drzewo (biorąc pod uwagę wskaźniki na otaczające środowisko). Ramki, które trzeba trzymać w pamięci też nie muszą tworzyć listy, tylko drzewo. Występuje to np. wówczas, gdy wynikiem procedury jest procedura. Zahaczamy tu o procedury wyższych rzędów, ale nie będziemy tego tematu teraz rozwijać.
Przykład: Niniejszy przykład pokazuje sytuację, gdy ramki powstałe w czasie wywołania procedury mogą być potrzebne po jego zakończeniu.
let f x = let g y = x * y in g;; let h = f 6;; h 7;;
TU GRAFIKA 12
Ramka zawierająca definicję procedury g powstaje w wyniku wywołania f 6, ale jest potrzebna później, do obliczenia h 7.\end{przyklad}
Przykład: Poniższy przykład ilustruje sposób przekazywania wielu argumentów, tak jak to jest realizowane --- po jednym na raz.
let g x = let a = x * x in fun y -> y + a;; let h x = (g x) x;; h 6;;
TU GRAFIKA 13