Programowanie funkcyjne/Struktury danych: Różnice pomiędzy wersjami
m Zastępowanie tekstu – „<math> ” na „<math>” |
|||
(Nie pokazano 20 wersji utworzonych przez 3 użytkowników) | |||
Linia 3: | Linia 3: | ||
Jak dotąd, definiowane przez nas pojęcia to wyłącznie stałe kilku wbudowanych typów i procedury. | Jak dotąd, definiowane przez nas pojęcia to wyłącznie stałe kilku wbudowanych typów i procedury. | ||
O ile możemy definiować złożone procedury, to nie jesteśmy narazie w stanie definiować złożonych danych. | O ile możemy definiować złożone procedury, to nie jesteśmy narazie w stanie definiować złożonych danych. | ||
Zajmiemy się tym w | Zajmiemy się tym w poniższym wykładzie. | ||
Zajmiemy się również metodyką tworzenia nowych ''typów'' danych. | Zajmiemy się również metodyką tworzenia nowych ''typów'' danych. | ||
</p> | </p> | ||
Linia 30: | Linia 30: | ||
<p align="justify"> | <p align="justify"> | ||
W Ocamlu | W Ocamlu środowisko przyporządkowuje każdej nazwie stałej jej wartość, która jest określonego typu. | ||
Dotyczy to również nazwanych procedur i w ich przypadku oznacza, że wiadomo jakiego typu są argumenty i wynik. | Dotyczy to również nazwanych procedur i w ich przypadku oznacza, że wiadomo jakiego typu są argumenty i wynik. | ||
W szczególności | W szczególności operacje arytmetyczne na liczbach całkowitych i zmiennopozycyjnych muszą mieć różne nazwy, | ||
gdyż ich argumenty są różnych typów. | gdyż ich argumenty są różnych typów. | ||
Operacje arytmetyczne na liczbach całkowitych zapisujemy w klasyczny sposób: | Operacje arytmetyczne na liczbach całkowitych zapisujemy w klasyczny sposób: | ||
Linia 42: | Linia 42: | ||
==Konstruktory== | ==Konstruktory== | ||
<p align="justify"> | <p align="justify"> | ||
Konstruktory | Konstruktory to szczególnego rodzaju operacje tworzące wartości określonych typów złożonych. | ||
Jednak konstruktor | Jednak konstruktor to nie tylko procedura tworząca złożoną wartość, to coś więcej. | ||
Dodatkowo są one odwracalne, tzn. z ich wyniku można jednoznacznie odtworzyć ich argumenty. | Dodatkowo są one odwracalne, tzn. z ich wyniku można jednoznacznie odtworzyć ich argumenty. | ||
Dzięki temu konstruktory nie tylko służą do tworzenia wartości typów | Dzięki temu konstruktory nie tylko służą do tworzenia wartości typów złożonych, | ||
ale również do rozbijania ich na tworzące je wartości typów prostszych. | ale również do rozbijania ich na tworzące je wartości typów prostszych. | ||
</p> | </p> | ||
Linia 56: | Linia 56: | ||
* każdą wartość danego typu można przedstawić w postaci wyrażenia złożonego z konstruktorów i stałych innych typów. | * każdą wartość danego typu można przedstawić w postaci wyrażenia złożonego z konstruktorów i stałych innych typów. | ||
Na razie nie znamy jeszcze żadnych konstruktorów. | |||
Zostaną one przedstawione dalej, wraz z kolejnymi typami złożonymi. | Zostaną one przedstawione dalej, wraz z kolejnymi typami złożonymi. | ||
Wówczas zobaczymy przykłady ich użycia | Wówczas zobaczymy przykłady ich użycia. | ||
</p> | </p> | ||
Linia 98: | Linia 98: | ||
</p> | </p> | ||
<definicja> ::= <u>let</u> <wzorzec> <u>=</u> <wyrażenie> | <definicja> ::= <u>let</u> <wzorzec> <u>=</u> <wyrażenie> | | ||
<u>let</u> [<u>rec</u>] <identyfikator> {<wzorzec>}<sup>*</sup> <u>=</u> <wyrażenie> | |||
<wyrażenie> ::= <u>match</u> <wyrażenie> <u>with</u> { <wzorzec> <u>-></u> <wyrażenie> <u>|</u> }<sup>*</sup> <wzorzec> <u>-></u> <wyrażenie> | <wyrażenie> ::= <u>match</u> <wyrażenie> <u>with</u> { <wzorzec> <u>-></u> <wyrażenie> <u>|</u> }<sup>*</sup> <wzorzec> <u>-></u> <wyrażenie> | | ||
<u>function</u> { <wzorzec> <u>-></u> <wyrażenie> <u>|</u> }<sup>*</sup> <wzorzec> <u>-></u> <wyrażenie> | |||
<p align="justify"> | <p align="justify"> | ||
Najprostsze zastosowanie wzorców | Najprostsze zastosowanie wzorców to definicje stałych. | ||
Po lewej stronie <tt>=</tt> możemy mieć zamiast identyfikatora wzorzec. | Po lewej stronie <tt>=</tt> możemy mieć zamiast identyfikatora wzorzec. | ||
Wartość wyrażenia po prawej stronie <tt>=</tt> jest dopasowywana do wzorca, a | Wartość wyrażenia po prawej stronie <tt>=</tt> jest dopasowywana do wzorca, a | ||
Linia 122: | Linia 122: | ||
<p align="justify"> | <p align="justify"> | ||
W definicjach procedur | W definicjach procedur nazwa procedury musi być identyfikatorem, ale parametry formalne mogą być wzorcami. | ||
Wówczas, w momencie wywołania procedury wartości argumentów są dopasowywane do parametrów formalnych. | Wówczas, w momencie wywołania procedury wartości argumentów są dopasowywane do parametrów formalnych. | ||
Wszystkie identyfikatory, którym w ten sposób są przypisywane wartości, trafiają do tymczasowego | Wszystkie identyfikatory, którym w ten sposób są przypisywane wartości, trafiają do tymczasowego | ||
Linia 132: | Linia 132: | ||
'''let''' f 6 9 = 42;; | '''let''' f 6 9 = 42;; | ||
''val f : int -> int -> int'' | ''val f : int -> int -> int'' | ||
f 6 9;; | f 6 9;; | ||
- : int = 42 | ''- : int = 42'' | ||
'''let''' p x _ = x + 1;; | '''let''' p x _ = x + 1;; | ||
''val p : int -> 'a -> int = <fun>'' | ''val p : int -> 'a -> int = <fun>'' | ||
p 6 9;; | p 6 9;; | ||
''- : int = 7'' | ''- : int = 7'' | ||
Linia 145: | Linia 145: | ||
W wyrażeniach <tt>match ... with ...</tt> wartość wyrażenia występującego po <tt>match</tt> | W wyrażeniach <tt>match ... with ...</tt> wartość wyrażenia występującego po <tt>match</tt> | ||
jest dopasowywana do kolejnych wzorców. | jest dopasowywana do kolejnych wzorców. | ||
Wzorzec, do którego ta wartość pasuje wskazuje równocześnie wyrażenie, | Wzorzec, do którego ta wartość pasuje, wskazuje równocześnie wyrażenie, | ||
którego wartość jest wartością całego wyrażenia <tt>match ... with ...</tt>. | którego wartość jest wartością całego wyrażenia <tt>match ... with ...</tt>. | ||
Jeśli dopasowywana wartość pasuje do kilku wzorców, to wybierany jest pierwszy z nich. | Jeśli dopasowywana wartość pasuje do kilku wzorców, to wybierany jest pierwszy z nich. | ||
Linia 163: | Linia 163: | ||
'''let''' '''rec''' silnia = | '''let''' '''rec''' silnia = | ||
'''function''' 1 -> 1 | x -> x * silnia (x - 1);; | |||
'''let''' '''rec''' silnia x = | '''let''' '''rec''' silnia x = | ||
'''match''' x '''with''' | |||
1 -> 1 | | |||
x -> x * silnia (x-1);; | |||
Wyrażenie <tt>match-with</tt> jest tylko lukrem syntaktycznym rozwijanym do zastosowania | Wyrażenie <tt>match-with</tt> jest tylko lukrem syntaktycznym rozwijanym do zastosowania | ||
Linia 186: | Linia 187: | ||
Jest to zbiór par, lub ogólniej <math>n</math>-tek wartości prostszych typów. | Jest to zbiór par, lub ogólniej <math>n</math>-tek wartości prostszych typów. | ||
Produkt kartezjański typów <math>t_1, \dots, t_n</math> jest oznaczany przez | Produkt kartezjański typów <math>t_1, \dots, t_n</math> jest oznaczany przez | ||
<math> t_1 * \dots * t_n</math>. | <math>t_1 * \dots * t_n</math>. | ||
Każdemu typowi produktowemu towarzyszy jeden konstruktor postaci: <math> (x_1, \dots, x_n)</math>. | Każdemu typowi produktowemu towarzyszy jeden konstruktor postaci: <math>(x_1, \dots, x_n)</math>. | ||
Wartością takiego konstruktora jest <math>n</math>-tka podanych wartości. | Wartością takiego konstruktora jest <math>n</math>-tka podanych wartości. | ||
Na wartościach produktów kartezjańskich jest określona równość. | Na wartościach produktów kartezjańskich jest określona równość. | ||
Linia 201: | Linia 202: | ||
(1, 2, 3);; | (1, 2, 3);; | ||
''- : int * int * int = (1, 2, 3)'' | ''- : int * int * int = (1, 2, 3)'' | ||
(42, (6.9, "ala"));; | (42, (6.9, "ala"));; | ||
''- : int * (float * string) = (42, (6.9, "ala"))'' | ''- : int * (float * string) = (42, (6.9, "ala"))'' | ||
'''let''' (x, s) = (4.5, "ala");; | '''let''' (x, s) = (4.5, "ala");; | ||
''val x : float = 4.5'' | ''val x : float = 4.5'' | ||
''val s : string = "ala"'' | ''val s : string = "ala"'' | ||
'''let''' fib n = | '''let''' fib n = | ||
'''let rec''' fibpom n = | '''let rec''' fibpom n = | ||
Linia 219: | Linia 220: | ||
'''let''' (a, b) = fibpom n | '''let''' (a, b) = fibpom n | ||
'''in''' a;; | '''in''' a;; | ||
''val fib : int -> int = <fun>'' | |||
'''let''' para x y = (x, y);; | '''let''' para x y = (x, y);; | ||
''val para : 'a -> 'b -> 'a * 'b = <fun>'' | |||
'''let''' rzut_x (x, _) = x;; | '''let''' rzut_x (x, _) = x;; | ||
''val rzut_x : 'a * 'b -> 'a = <fun>'' | |||
'''let''' rzut_y (_, y) = y;; | '''let''' rzut_y (_, y) = y;; | ||
''val rzut_y : 'a * 'b -> 'b = <fun>'' | |||
<p align="justify"> | <p align="justify"> | ||
Linia 230: | Linia 237: | ||
(2, 3, 4);; | (2, 3, 4);; | ||
''- : int * int * int = (2, 3, 4)'' | ''- : int * int * int = (2, 3, 4)'' | ||
(2, (3, 4));; | (2, (3, 4));; | ||
''- : int * (int * int) = (2, (3, 4))'' | ''- : int * (int * int) = (2, (3, 4))'' | ||
((2, 3), 4);; | ((2, 3), 4);; | ||
''- : (int * int) * int = ((2, 3), 4)'' | ''- : (int * int) * int = ((2, 3), 4)'' | ||
<p align="justify"> | <p align="justify"> | ||
Jednoelementowe <math>n</math>-tki wartości typu <math>t</math> | Jednoelementowe <math>n</math>-tki wartości typu <math>t</math> to po prostu wartości typu <math>t</math>, | ||
<math>(x) = x</math>. | <math>(x) = x</math>. | ||
Istnieje "jedynka" produktu kartezjańskiego (odpowiednik typu <tt>void</tt>), typ <tt>unit</tt>. | Istnieje "jedynka" produktu kartezjańskiego (odpowiednik typu <tt>void</tt>), typ <tt>unit</tt>. | ||
Linia 247: | Linia 254: | ||
==Listy== | ==Listy== | ||
<p align="justify"> | <p align="justify"> | ||
Listy to ciągi elementów tego samego typu (skończone | Listy to ciągi elementów tego samego typu (skończone lub nieskończone, ale od pewnego miejsca okresowe). | ||
Typ listy elementów typu <tt>t</tt> oznaczamy przez <tt>t list</tt>. | Typ listy elementów typu <tt>t</tt> oznaczamy przez <tt>t list</tt>. | ||
Pierwszy element listy przyjęło się nazywać ''głową'', a | Pierwszy element listy przyjęło się nazywać ''głową'', a | ||
listę złożoną z wszystkich pozostałych elementów listy przyjęło się nazywać ogonem. | listę złożoną z wszystkich pozostałych elementów listy przyjęło się nazywać ''ogonem''. | ||
Tak więc każda niepusta lista (tak jak wąż ;-) składa się z głowy i ogona. | Tak więc każda niepusta lista (tak jak wąż ;-) składa się z głowy i ogona. | ||
</p> | </p> | ||
Linia 263: | Linia 270: | ||
<p align="justify"> | <p align="justify"> | ||
Chcąc podać listę <math>n</math> elementową nie musimy pisać: | Chcąc podać listę <math>n</math> elementową nie musimy pisać: | ||
<math> x_1 :: (x_2 :: \dots :: (x_n :: [])\dots)</math>, lecz możemy użyć skrótu notacyjnego: | <math>x_1 :: (x_2 :: \dots :: (x_n :: [])\dots)</math>, lecz możemy użyć skrótu notacyjnego: | ||
<math>[x_1; x_2; \dots; x_n]</math>. | <math>[x_1; x_2; \dots; x_n]</math>. | ||
Dostępna jest też operacja sklejania list <tt>@</tt>. | Dostępna jest też operacja sklejania list <tt>@</tt>. | ||
Należy pamiętać, że operacje <tt>::</tt> i <tt>[]</tt> obliczane są w czasie stałym, | |||
natomiast <tt>@</tt> w czasie proporcjonalnym do długości pierwszego argumentu. | natomiast <tt>@</tt> w czasie proporcjonalnym do długości pierwszego argumentu. | ||
</p> | </p> | ||
Linia 281: | Linia 288: | ||
[];; | [];; | ||
''- : 'a list = []'' | ''- : 'a list = []'' | ||
1::2::3::[];; | 1::2::3::[];; | ||
''- : int list = [1; 2; 3]'' | ''- : int list = [1; 2; 3]'' | ||
[1; 2; 3; 4];; | [1; 2; 3; 4];; | ||
''- : int list = [1; 2; 3; 4]'' | ''- : int list = [1; 2; 3; 4]'' | ||
[1;2;3] @ [4;5;6];; | [1;2;3] @ [4;5;6];; | ||
''- : int list = [1; 2; 3; 4; 5; 6]'' | ''- : int list = [1; 2; 3; 4; 5; 6]'' | ||
["To"; "ci"; "dopiero"; "lista"];; | ["To"; "ci"; "dopiero"; "lista"];; | ||
''- : string list = ["To"; "ci"; "dopiero"; "lista"]'' | ''- : string list = ["To"; "ci"; "dopiero"; "lista"]'' | ||
'''let''' '''rec''' length l = | '''let''' '''rec''' length l = | ||
'''match''' l '''with''' | |||
[] -> 0 | | |||
_::t -> 1 + length t;; | |||
'''let''' '''rec''' sumuj l = | '''let''' '''rec''' sumuj l = | ||
'''match''' l '''with''' | |||
[] -> 0 | | |||
h::t -> h + sumuj t;; | |||
'''let''' '''rec''' listapar2paralist = | '''let''' '''rec''' listapar2paralist = | ||
'''function''' | |||
[] -> ([], []) | | |||
(x, y)::t -> | |||
'''let''' (l1, l2) = listapar2paralist t | |||
'''in''' (x :: l1, y :: l2);; | |||
<p align="justify"> | <p align="justify"> | ||
Możliwe jest definiowanie list nieskończonych. | Możliwe jest definiowanie list nieskończonych. | ||
Należy jednak zdawać sobie sprawę, że listy są implementowane jako listy jednokierunkowe. | Należy jednak zdawać sobie sprawę, że listy są implementowane jako wskaźnikowe listy jednokierunkowe. | ||
Dlatego też lista nieskończona może mieć co najwyżej postać cyklu z "ogonkiem". | Dlatego też lista nieskończona może mieć co najwyżej postać cyklu z "ogonkiem". | ||
Poniższy przykład pokazuje jak definiować listy nieskończone. | Poniższy przykład pokazuje jak definiować listy nieskończone. | ||
Linia 323: | Linia 330: | ||
'''let rec''' jedynki = 1::jedynki;; | '''let rec''' jedynki = 1::jedynki;; | ||
''val jedynki : int list = [1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; ...]'' | ''val jedynki : int list = [1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; ...]'' | ||
'''let rec''' cykl = 1 :: 0 :: -1 :: 0 :: cykl;; | '''let rec''' cykl = 1 :: 0 :: -1 :: 0 :: cykl;; | ||
''val cykl : int list = [1; 0; -1; 0; 1; 0; -1; 0; ...]'' | ''val cykl : int list = [1; 0; -1; 0; 1; 0; -1; 0; ...]'' | ||
[1;2;3] @ cykl;; | [1;2;3] @ cykl;; | ||
''- : int list = [1; 2; 3; 1; 0; -1; 0; 1; 0; -1; 0; ...]'' | ''- : int list = [1; 2; 3; 1; 0; -1; 0; 1; 0; -1; 0; ...]'' | ||
cykl @ [1;2;3];; | cykl @ [1;2;3];; | ||
''Stack overflow during evaluation (looping recursion?).'' | ''Stack overflow during evaluation (looping recursion?).'' | ||
<p align="justify"> | <p align="justify"> | ||
Moduł <tt>List</tt> zawiera wiele poręcznych procedur operujących | Moduł <tt>List</tt> zawiera wiele poręcznych procedur operujących na listach. | ||
O modułach | O modułach będziemy mówić dokładniej w jednym z wykładów. | ||
W tej chwili wystarczy nam wiedzieć, że aby korzystać z pojęć zdefiniowanych w module <tt>List</tt>, | W tej chwili wystarczy nam wiedzieć, że aby korzystać z pojęć zdefiniowanych w module <tt>List</tt>, | ||
wystarczy wprowadzić polecenie <tt>open List;;</tt> lub przed każdą nazwą pojęcia dodać przedrostek | wystarczy wprowadzić polecenie <tt>open List;;</tt> lub przed każdą nazwą pojęcia dodać przedrostek | ||
postaci <tt>List.</tt> | postaci <tt>List.</tt> | ||
Moduł ten implementuje m. | Moduł ten implementuje m.in. następujące operacje: | ||
</p> | </p> | ||
Linia 352: | Linia 359: | ||
length ["To"; "ci"; "dopiero"; "lista"];; | length ["To"; "ci"; "dopiero"; "lista"];; | ||
''- : int = 4'' | ''- : int = 4'' | ||
hd ["To"; "ci"; "dopiero"; "lista"];; | hd ["To"; "ci"; "dopiero"; "lista"];; | ||
''- : string = "To"'' | ''- : string = "To"'' | ||
tl ["To"; "ci"; "dopiero"; "lista"];; | tl ["To"; "ci"; "dopiero"; "lista"];; | ||
''- : string list = ["ci"; "dopiero"; "lista"]'' | ''- : string list = ["ci"; "dopiero"; "lista"]'' | ||
List.rev ["To"; "ci"; "dopiero"; "lista"];; | List.rev ["To"; "ci"; "dopiero"; "lista"];; | ||
''- : string list = ["lista"; "dopiero"; "ci"; "To"]'' | ''- : string list = ["lista"; "dopiero"; "ci"; "To"]'' | ||
nth ["To"; "ci"; "dopiero"; "lista"] 2;; | nth ["To"; "ci"; "dopiero"; "lista"] 2;; | ||
''- : string = "dopiero"'' | ''- : string = "dopiero"'' | ||
List.append [1; 2; 3] [4; 5];; | List.append [1; 2; 3] [4; 5];; | ||
''- : int list = [1; 2; 3; 4; 5]'' | ''- : int list = [1; 2; 3; 4; 5]'' | ||
Linia 367: | Linia 379: | ||
Zwykle kompilator Ocamla sam jest w stanie wywnioskować jakiego typu są wyrażenia. | Zwykle kompilator Ocamla sam jest w stanie wywnioskować jakiego typu są wyrażenia. | ||
Istnieje jednak możliwość nazywania typów i określania jakiego typu się spodziewamy. | Istnieje jednak możliwość nazywania typów i określania jakiego typu się spodziewamy. | ||
Wówczas kompilator sprawdza czy dane wyrażenie faktycznie jest takiego typu, jak chcieliśmy, a zamiast pełnego rozwinięcia typu może używać podanej przez nas | Wówczas kompilator sprawdza, czy dane wyrażenie faktycznie jest takiego typu, jak chcieliśmy, a zamiast pełnego rozwinięcia typu może używać podanej przez nas krótszej nazwy. | ||
Niektóre typy danych | Niektóre typy danych można wprowadzić tylko poprzez ich deklarację. | ||
Są to te typy, których deklaracje wprowadzają nowe konstruktory. | Są to te typy, których deklaracje wprowadzają nowe konstruktory. | ||
</p> | </p> | ||
<p align="justify"> | <p align="justify"> | ||
Możliwe jest też zdefiniowanie typów sparametryzowanych, tzn. takich, w których typy niektórych ich składowych są podawane jako. | Możliwe jest też zdefiniowanie typów sparametryzowanych, tzn. takich, w których typy niektórych ich składowych są podawane jako parametry typu. | ||
Elementy struktury danych określone jako parametry mogą być dowolnego typu, | |||
ale wszystkie elementy określone przez ten sam parametr muszą być (w danej wartości) tego samego typu. | |||
(Uważny czytelnik zapewne domyśla się, że przykładem typu sparametryzowanego jest lista. | (Uważny czytelnik zapewne domyśla się, że przykładem typu sparametryzowanego jest lista. | ||
Parametrem typu listowego jest typ elementów listy.) | Parametrem typu listowego jest typ elementów listy.) | ||
Linia 460: | Linia 474: | ||
'''type''' ułamek = { licznik : int ; mianownik : int };; | '''type''' ułamek = { licznik : int ; mianownik : int };; | ||
''type ulamek = { licznik : int; mianownik : int; }'' | |||
'''let''' q = {licznik = 3; mianownik = 4 | |||
'''let''' q = {licznik = 3; mianownik = 4};; | |||
''val q : ulamek = {licznik = 3; mianownik = 4}'' | |||
'''let''' {licznik = a ; mianownik = b} = q;; | '''let''' {licznik = a ; mianownik = b} = q;; | ||
''val a : int = 3'' | |||
''val b : int = 4'' | |||
'''let''' {licznik = c} = q;; | '''let''' {licznik = c} = q;; | ||
''val c : int = 3'' | |||
q.licznik;; | q.licznik;; | ||
''- : int = 3'' | ''- : int = 3'' | ||
Linia 478: | Linia 496: | ||
==Warianty== | ==Warianty== | ||
<p align="justify"> | <p align="justify"> | ||
W Ocamlu istnieje również odpowiednik unii | W Ocamlu istnieje również odpowiednik unii -- typy wariantowe, zwane też typami algebraicznymi. | ||
Deklarujemy je w następujący sposób: | Deklarujemy je w następujący sposób: | ||
</p> | </p> | ||
Linia 492: | Linia 510: | ||
<p align="justify"> | <p align="justify"> | ||
Każdy zadeklarowany wariant wprowadza konstruktor o podanej nazwie. | Każdy zadeklarowany wariant wprowadza konstruktor o podanej nazwie. | ||
Konstruktor ten może być bezparametrowy | Konstruktor ten może być bezparametrowy lub może mieć argumenty. | ||
Formalnie konstruktor taki może mieć co najwyżej jeden argument, ale zawsze może to być argument odpowiedniego typu produktowego. | Formalnie konstruktor taki może mieć co najwyżej jeden argument, ale zawsze może to być argument odpowiedniego typu produktowego. | ||
Konstruktorów typów wariantowych używamy w następujący sposób: | Konstruktorów typów wariantowych używamy w następujący sposób: | ||
Linia 579: | Linia 597: | ||
<p align="justify"> | <p align="justify"> | ||
Dopasowywana wartość jest przypisywana identyfikatorowi po prawej stronie <tt>as</tt> | Dopasowywana wartość jest przypisywana identyfikatorowi po prawej stronie <tt>as</tt> oraz jest dopasowywana do wzorca po lewej stronie. | ||
</p> | </p> | ||
Linia 603: | Linia 621: | ||
Jest to nietypowy typ, gdyż można na bieżąco rozszerzać zestaw wariantów tworzących ten typ. | Jest to nietypowy typ, gdyż można na bieżąco rozszerzać zestaw wariantów tworzących ten typ. | ||
Wartości tego typu niosą informację o wyjątkowych sytuacjach uniemożliwiających wykonanie obliczeń. | Wartości tego typu niosą informację o wyjątkowych sytuacjach uniemożliwiających wykonanie obliczeń. | ||
Nowe warianty typu <tt>exn</tt> możemy deklarować w | Nowe warianty typu <tt>exn</tt> możemy deklarować w następujący sposób: | ||
</p> | </p> | ||
Linia 611: | Linia 629: | ||
<p align="justify"> | <p align="justify"> | ||
Z wyjątkami można robić dwie podstawowe rzeczy: podnosić i przechwytywać. | Z wyjątkami można robić dwie podstawowe rzeczy: podnosić i przechwytywać. | ||
Podniesienie wyjątku | Podniesienie wyjątku to wyrażenie, którego obliczenie "nie udaje się", | ||
a porażce tej towarzyszy wartość | a porażce tej towarzyszy wartość podnoszonego wyjątku (typu <tt>exn</tt>). | ||
Jeśli obliczenie dowolnego podwyrażenia nie | Jeśli obliczenie dowolnego podwyrażenia nie udaje się na skutek podniesienia wyjątku, | ||
to tak samo nie udaje się obliczenie całego wyrażenia. | to tak samo nie udaje się obliczenie całego wyrażenia. | ||
Tak więc wyjątek | Tak więc wyjątek to rodzaj propagującego się "błędu w obliczeniach". | ||
Podniesienie wyjątku ma następującą postać, | Podniesienie wyjątku ma następującą postać, | ||
przy czym argument operacji <tt>raise</tt> musi być typu <tt>exn</tt>: | przy czym argument operacji <tt>raise</tt> musi być typu <tt>exn</tt>: | ||
Linia 680: | Linia 698: | ||
==Przykłady ciekawych struktur danych== | ==Przykłady ciekawych struktur danych== | ||
===Kolejki FIFO=== | ===Kolejki FIFO=== | ||
<p align="justify"> | |||
Kolejka FIFO (ang. ''first in, first out'') to taka kolejka, do której możemy wkładać elementy na koniec i wyjmować elementy z początku. | Kolejka FIFO (ang. ''first in, first out'') to taka kolejka, do której możemy wkładać elementy na koniec i wyjmować elementy z początku. | ||
Dodatkowo możemy podglądać jaki element jest pierwszy w kolejce. | Dodatkowo możemy podglądać jaki element jest pierwszy w kolejce. | ||
Linia 686: | Linia 705: | ||
* listę, którą będziemy nazywać "tylną" i | * listę, którą będziemy nazywać "tylną" i | ||
* liczba elementów w kolejce. | * liczba elementów w kolejce. | ||
Lista przednia <math>[x_1; \dots; x_k]</math>, lista tylna <math>[y_1; \dots; y_l]</math> reprezentują kolejkę zawierającą <math>k+l</math> elementów: <math> x_1, \dots, x_k, y_l, \dots, y_1</math>. | Lista przednia <math>[x_1; \dots; x_k]</math>, lista tylna <math>[y_1; \dots; y_l]</math> reprezentują kolejkę zawierającą <math>k+l</math> elementów: <math>x_1, \dots, x_k, y_l, \dots, y_1</math>. | ||
Tak więc każdy element w kolejce występuje na jednej z list, wszystkie elementy z listy przedniej poprzedzają elementy z listy tylnej, elementy na liście przedniej występują zgodnie z kolejnością w kolejce, a na liście tylnej w odwrotnej kolejności. | Tak więc każdy element w kolejce występuje na jednej z list, wszystkie elementy z listy przedniej poprzedzają elementy z listy tylnej, elementy na liście przedniej występują zgodnie z kolejnością w kolejce, a na liście tylnej w odwrotnej kolejności. | ||
Dodatkowo zakładamy, że jeżeli lista przednia jest pusta, to tylna też musi być pusta. | Dodatkowo zakładamy, że jeżeli lista przednia jest pusta, to tylna też musi być pusta. | ||
Wyjmując element z kolejki, wyjmujemy go z listy przedniej, a wkładając, wkładamy do tylnej. | Wyjmując element z kolejki, wyjmujemy go z listy przedniej, a wkładając, wkładamy do tylnej. | ||
Gdy zabraknie elementów do wyjmowania, | Gdy zabraknie elementów do wyjmowania, "przelewamy" elementy z listy tylnej do przedniej, odwracając ich kolejność na liście. | ||
</p> | |||
'''type''' 'a kolejka = {front: 'a list; back: 'a list; size: int};; | '''type''' 'a kolejka = {front: 'a list; back: 'a list; size: int};; | ||
Linia 724: | Linia 744: | ||
''val remove : 'a kolejka -> 'a kolejka = <fun>'' | ''val remove : 'a kolejka -> 'a kolejka = <fun>'' | ||
Zanalizujmy koszt | <p align="justify"> | ||
Dla pustej kolejki funkcja amortyzacji jest równa 0. Procedura <tt>put</tt> ma koszt stały i powoduje | Operacja <tt>balance</tt> zapewnia, że jeżeli kolejka nie jest pusta, to jej lista przednia nie jest pusta. | ||
Zanalizujmy koszt operacji na kolejkach (zakładając, że pamiętamy jedną kolejkę na raz). | |||
Pomijając koszt procedury <tt>balance</tt>, wszystkie operacje na kolejce mają koszt stały. | |||
Pokażemy, że koszt zamortyzowany procedury <tt>balance</tt> jest też stały. | |||
</p> | |||
<p align="justify"> | |||
Jako funkcję amortyzacji przyjmujemy długość listy tylnej. | |||
Dla pustej kolejki funkcja amortyzacji jest równa 0. | |||
Procedura <tt>put</tt> ma koszt rzeczywisty stały i powoduje zwiększenie funkcji amortyzacji o 1, co daje stały koszt zamortyzowany. | |||
Procedura <tt>remove</tt> może mieć koszt rzeczywisty liniowy ze względu na odwracanie elementów z listy tylnej. | |||
Jednak równocześnie zmniejsza ona funkcję amortyzacji do 0, czyli o tyle, ile trwa odwracanie listy. | |||
Tak więc koszt zamortyzowany procedury <tt>remove</tt> jest stały. | |||
</p> | |||
===Zbiory skończone jako drzewa BST=== | ===Zbiory skończone jako drzewa BST=== | ||
<p align="justify"> | |||
Poniżej przedstawiamy implementację drzew BST. | |||
Typ reprezentujący drzewa binarne możemy zadeklarować nastepująco: | |||
</p> | |||
'''type''' 'a finset = Empty | Node '''of''' ('a * 'a finset * 'a finset);; | '''type''' 'a finset = Empty | Node '''of''' ('a * 'a finset * 'a finset);; | ||
''type 'a finset = Empty | Node of ('a * 'a finset * 'a finset)'' | |||
Zwróćmy uwagę, że jest to sparametryzowana deklaracja rekurencyjna. Czymkolwiek by | |||
<p align="justify"> | |||
Zwróćmy uwagę, że jest to sparametryzowana deklaracja rekurencyjna. | |||
Czymkolwiek by był parametr <tt>`a</tt>, wszystkie węzły drzewa muszą zawierać wartości tego samego typu. | |||
</p> | |||
(* Wyjątek podnoszony, gdy badamy wartość spoza dziedziny. *) | (* Wyjątek podnoszony, gdy badamy wartość spoza dziedziny. *) | ||
'''exception''' Undefined;; | '''exception''' Undefined;; | ||
''exception Undefined'' | |||
(* Pusty zbiór to puste drzewo *) | (* Pusty zbiór to puste drzewo *) | ||
'''let''' empty_finset = Empty;; | '''let''' empty_finset = Empty;; | ||
''val empty_finset : 'a finset = Empty'' | |||
(* Znajdź poddrzewo o zadanym kluczu *) | (* Znajdź poddrzewo o zadanym kluczu *) | ||
(* (jeśli nie ma, to wynikiem jest puste drzewo). *) | (* (jeśli nie ma, to wynikiem jest puste drzewo). *) | ||
'''let''' '''rec''' find s e = | '''let''' '''rec''' find s e = | ||
'''match''' s '''with''' | |||
Empty -> Empty | | |||
Node (k, l, r) -> | |||
'''if''' e = k '''then''' s | |||
'''else''' '''if''' e < k '''then''' find l e | |||
'''else''' find r e;; | |||
''val find : 'a finset -> 'a -> 'a finset = <fun>'' | |||
(* Predykat charakterystyczny zbioru. *) | (* Predykat charakterystyczny zbioru. *) | ||
'''let''' member s e = not (find s e = Empty);; | '''let''' member s e = not (find s e = Empty);; | ||
''val member : 'a finset -> 'a -> bool = <fun>'' | |||
(* Wstawienie elementu do zbioru *) | (* Wstawienie elementu do zbioru *) | ||
'''let''' '''rec''' insert s e = | '''let''' '''rec''' insert s e = | ||
'''match''' s '''with''' | |||
Empty -> Node (e, Empty, Empty) | | |||
Node (k, l, r) -> | |||
'''if''' e = k '''then''' | |||
s | |||
'''else''' if e < k '''then''' | |||
Node (k, insert l e, r) | |||
'''else''' | |||
Node (k, l, insert r e);; | |||
''val insert : 'a finset -> 'a -> 'a finset = <fun>'' | |||
(* Najmniejszy element *) | (* Najmniejszy element *) | ||
'''let''' '''rec''' min s = | '''let''' '''rec''' min s = | ||
'''match''' s '''with''' | |||
Node (e, Empty, _) -> e | | |||
Node (_, l, _) -> min l | | |||
Empty -> '''raise''' Undefined;; | |||
''val min : 'a finset -> 'a = <fun>'' | |||
(* Największy element *) | (* Największy element *) | ||
'''let''' '''rec''' max s = | '''let''' '''rec''' max s = | ||
'''match''' s '''with''' | |||
Node (e, _, Empty) -> e | | |||
Node (_, _, r) -> max r | | |||
Empty -> '''raise''' Undefined;; | |||
''val max : 'a finset -> 'a = <fun>'' | |||
(* Usunięcie elementu z listy *) | (* Usunięcie elementu z listy *) | ||
'''let''' '''rec''' remove s e = | '''let''' '''rec''' remove s e = | ||
'''match''' s '''with''' | |||
Empty -> s | | |||
Node (k, l, r) -> | |||
'''if''' e = k '''then''' | |||
'''match''' (l, r) '''with''' | |||
(Empty, Empty) -> Empty | | |||
(Empty, _) -> | |||
'''let''' m = min r | |||
'''in''' Node (m, l, remove r m) | | |||
_ -> | |||
'''let''' m = max l | |||
'''in''' Node (m, remove l m, r) | |||
'''else''' '''if''' e < k '''then''' | |||
Node (k, remove l e, r) | |||
'''else''' | |||
Node (k, l, remove r e);; | |||
''val remove : 'a finset -> 'a -> 'a finset = <fun>'' | |||
== | == Ćwiczenia == | ||
[[Programowanie funkcyjne/Struktury danych/Ćwiczenia | | [[Programowanie funkcyjne/Struktury danych/Ćwiczenia |Ćwiczenia]] |
Aktualna wersja na dzień 22:11, 11 wrz 2023
Wstęp
Jak dotąd, definiowane przez nas pojęcia to wyłącznie stałe kilku wbudowanych typów i procedury. O ile możemy definiować złożone procedury, to nie jesteśmy narazie w stanie definiować złożonych danych. Zajmiemy się tym w poniższym wykładzie. Zajmiemy się również metodyką tworzenia nowych typów danych.
Pojęcie typu danych jest zapewne dobrze znane Czytelnikowi z innych języków programowania. Spróbujmy jednak, dla porządku, krótko scharakteryzować to pojęcie. Typ danych to zbiór wartości wraz z zestawem podstawowych operacji na tych wartościach.
W każdym języku programowania dostępna jest pewna liczba wbudowanych typów danych, takich jak liczby całkowite, wartości logiczne itp. Są one nazywane typami prostymi. Oprócz typów prostych mamy typy złożone, które zawierają w sobie prostsze typy. W tym wykładzie poznamy różne konstrukcje pozwalające na tworzenie rozmaitych typów złożonych.
Typy wbudowane
Poznaliśmy już kilka podstawowych typów danych wbudowanych w język Ocaml wraz z operacjami na nich. Są to: bool, int, float, char i string.
W Ocamlu środowisko przyporządkowuje każdej nazwie stałej jej wartość, która jest określonego typu. Dotyczy to również nazwanych procedur i w ich przypadku oznacza, że wiadomo jakiego typu są argumenty i wynik. W szczególności operacje arytmetyczne na liczbach całkowitych i zmiennopozycyjnych muszą mieć różne nazwy, gdyż ich argumenty są różnych typów. Operacje arytmetyczne na liczbach całkowitych zapisujemy w klasyczny sposób: +, *, itd., a operacje na liczbach zmiennopozycyjnych mają dodaną w nazwie kropkę: +., *. itd. Niestety, zapewne na początku często będą się zdarzać błedy wynikające z mylenia tych operacji.
Konstruktory
Konstruktory to szczególnego rodzaju operacje tworzące wartości określonych typów złożonych. Jednak konstruktor to nie tylko procedura tworząca złożoną wartość, to coś więcej. Dodatkowo są one odwracalne, tzn. z ich wyniku można jednoznacznie odtworzyć ich argumenty. Dzięki temu konstruktory nie tylko służą do tworzenia wartości typów złożonych, ale również do rozbijania ich na tworzące je wartości typów prostszych.
Formalnie, konstruktory (wartości określonego typu) mają następujące własności:
- są one różnowartościowe,
- różne konstruktory wartości tego samego typu mają różne wartości, tzn. przeciwdziedziny różnych konstruktorów są rozłączne,
- wszystkie konstruktory wartości danego typu są łącznie "na", tzn. każdą wartość danego typu można skonstruować,
- każdą wartość danego typu można przedstawić w postaci wyrażenia złożonego z konstruktorów i stałych innych typów.
Na razie nie znamy jeszcze żadnych konstruktorów. Zostaną one przedstawione dalej, wraz z kolejnymi typami złożonymi. Wówczas zobaczymy przykłady ich użycia.
Wzorce
Zanim przedstawimy sposoby konstruowania złożonych typów danych, wprowadzimy pojęcie wzorca. Co to jest wzorzec? Wzorzec jest to rodzaj "wyrażenia". Do budowy wzorców możemy używać konstruktorów i identyfikatorów. Wzorce pojawiają się w takich kontekstach, że zawsze dopasowywana jest do nich wartość pewnego wyrażenia. Identyfikatory występujące we wzorcach są nazwami nowych stałych wprowadzanych do środowiska. Jeśli wartość wyrażenia dopasowywanego do wzorca "pasuje", to występującym we wzorcu identyfikatorom przypisywane są w środowisku wartości odpowiadających im składowych dopasowywanej wartości. Identyfikatory występujące we wzorcu nie mogą się powtarzać, gdyż każde wystąpienie identyfikatora odpowiada za inny fragment dopasowywanej wartości. Wzorce będą stanowić podstawowy mechanizm dekompozycji wartości złożonych typów, jednak ich zastosowanie nie ogranicza się tylko do tego.
Przedstawiając kolejne typy złożone będziemy przedstawiać również towarzyszące im konstruktory i wzorce. Istnieje jednak kilka podstawowych wzorców, które mogą być używane dla wartości dowolnego typu:
<wzorzec> ::= <identyfikator> | _ | <stała> | ...
- identyfikator -- pasuje do każdej wartości, nazwie przyporządkowywana jest dopasowywana wartość,
- _ -- tak jak identyfikator, tylko nie wprowadza żadnej nowej nazwy,
- stała (np. liczbowa) -- pasuje tylko do samej siebie.
Wzorca _ używamy wówczas, gdy interesuje nas sam fakt, że wartość pasuje do wzorca, a nie chcemy wprowadzać do środowiska nowych nazw.
Wzorców możemy używać w wyrażeniach match ... with ... oraz w definicjach procedur (zarówno nazwanych, jak i -abstrakcji):
<definicja> ::= let <wzorzec> = <wyrażenie> | let [rec] <identyfikator> {<wzorzec>}* = <wyrażenie> <wyrażenie> ::= match <wyrażenie> with { <wzorzec> -> <wyrażenie> | }* <wzorzec> -> <wyrażenie> | function { <wzorzec> -> <wyrażenie> | }* <wzorzec> -> <wyrażenie>
Najprostsze zastosowanie wzorców to definicje stałych. Po lewej stronie = możemy mieć zamiast identyfikatora wzorzec. Wartość wyrażenia po prawej stronie = jest dopasowywana do wzorca, a wszystkie identyfikatory, którym w ten sposób są przypisywane wartości, trafiają do środowiska.
Przykład [Wzorce w definicjach stałych]
3;; - : int = 3 let _ = 3;; - : int = 3 let a = 42;; val a : int = 42
W definicjach procedur nazwa procedury musi być identyfikatorem, ale parametry formalne mogą być wzorcami. Wówczas, w momencie wywołania procedury wartości argumentów są dopasowywane do parametrów formalnych. Wszystkie identyfikatory, którym w ten sposób są przypisywane wartości, trafiają do tymczasowego środowiska powstającego dla obliczenia wartości procedury.
Przykład [Wzorce jako argumenty procedur]
let f 6 9 = 42;; val f : int -> int -> int f 6 9;; - : int = 42 let p x _ = x + 1;; val p : int -> 'a -> int = <fun> p 6 9;; - : int = 7
W wyrażeniach match ... with ... wartość wyrażenia występującego po match jest dopasowywana do kolejnych wzorców. Wzorzec, do którego ta wartość pasuje, wskazuje równocześnie wyrażenie, którego wartość jest wartością całego wyrażenia match ... with .... Jeśli dopasowywana wartość pasuje do kilku wzorców, to wybierany jest pierwszy z nich. Jeżeli nie pasuje ona do żadnego z wzorców, to zgłaszany jest błąd (wyjątek).
Podobnie działa dopasowywanie wzorców w -abstrakcjach. Argument procedury jest dopasowywany do kolejnych wzorców. Wzorzec, do którego wartość argumentu pasuje wskazuje równocześnie wyrażenie, którego wartość jest wynikiem procedury. Jeśli wartość argumentu pasuje do kilku wzorców, to wybierany jest pierwszy z nich. Jeżeli nie pasuje ona do żadnego z wzorców, to zgłaszany jest błąd (wyjątek).
Przykład [Konstrukcja match-with]
let rec silnia = function 1 -> 1 | x -> x * silnia (x - 1);; let rec silnia x = match x with 1 -> 1 | x -> x * silnia (x-1);;
Wyrażenie match-with jest tylko lukrem syntaktycznym rozwijanym do zastosowania odpowiedniej -abstrakcji. Ilustruje to poniższy przykład.
Przykład [Rozwinięcie match-with]
let rec silnia x = (function 1 -> 1 | x -> x * silnia (x-1) ) x;;
Produkty kartezjańskie
Produkt kartezjański jako typ jest dokładnie tym, czym jest w matematyce. Jest to zbiór par, lub ogólniej -tek wartości prostszych typów. Produkt kartezjański typów jest oznaczany przez . Każdemu typowi produktowemu towarzyszy jeden konstruktor postaci: . Wartością takiego konstruktora jest -tka podanych wartości. Na wartościach produktów kartezjańskich jest określona równość. Dwie -tki są są równe, jeśli ich odpowiadające sobie współrzędne są równe.
<wyrażenie> ::= ( [ <wyrażenie> { , <wyrażenie> }* ] ) <wzorzec> ::= ( [ <wzorzec> { , <wzorzec> }* ] )
Przykład [Produkty kartezjańskie]
(1, 2, 3);; - : int * int * int = (1, 2, 3) (42, (6.9, "ala"));; - : int * (float * string) = (42, (6.9, "ala")) let (x, s) = (4.5, "ala");; val x : float = 4.5 val s : string = "ala" let fib n = let rec fibpom n = if n = 0 then (0, 1) else let (a, b) = fibpom (n - 1) in (b, a + b) in let (a, b) = fibpom n in a;; val fib : int -> int = <fun> let para x y = (x, y);; val para : 'a -> 'b -> 'a * 'b = <fun> let rzut_x (x, _) = x;; val rzut_x : 'a * 'b -> 'a = <fun> let rzut_y (_, y) = y;; val rzut_y : 'a * 'b -> 'b = <fun>
Produkt kartezjański nie jest łączny, tzn. poniższe wyrażenia są różnych typów:
(2, 3, 4);; - : int * int * int = (2, 3, 4) (2, (3, 4));; - : int * (int * int) = (2, (3, 4)) ((2, 3), 4);; - : (int * int) * int = ((2, 3), 4)
Jednoelementowe -tki wartości typu to po prostu wartości typu , . Istnieje "jedynka" produktu kartezjańskiego (odpowiednik typu void), typ unit. Jedyną wartością tego typu jest (). Jednak t * unit to inny typ niż t.
Listy
Listy to ciągi elementów tego samego typu (skończone lub nieskończone, ale od pewnego miejsca okresowe). Typ listy elementów typu t oznaczamy przez t list. Pierwszy element listy przyjęło się nazywać głową, a listę złożoną z wszystkich pozostałych elementów listy przyjęło się nazywać ogonem. Tak więc każda niepusta lista (tak jak wąż ;-) składa się z głowy i ogona.
Typowi listowemu towarzyszą dwa konstruktory: h::t i []. [] to konstruktor listy pustej, a h::t tworzy listę, której głową jest h, a ogonem jest t.
Chcąc podać listę elementową nie musimy pisać: , lecz możemy użyć skrótu notacyjnego: . Dostępna jest też operacja sklejania list @. Należy pamiętać, że operacje :: i [] obliczane są w czasie stałym, natomiast @ w czasie proporcjonalnym do długości pierwszego argumentu.
Na listach jest określona równość. Dwie listy są równe, jeżeli są równej długości i odpowiadające sobie elementy są równe.
Przykład [Listy]
[];; - : 'a list = [] 1::2::3::[];; - : int list = [1; 2; 3] [1; 2; 3; 4];; - : int list = [1; 2; 3; 4] [1;2;3] @ [4;5;6];; - : int list = [1; 2; 3; 4; 5; 6] ["To"; "ci"; "dopiero"; "lista"];; - : string list = ["To"; "ci"; "dopiero"; "lista"] let rec length l = match l with [] -> 0 | _::t -> 1 + length t;; let rec sumuj l = match l with [] -> 0 | h::t -> h + sumuj t;; let rec listapar2paralist = function [] -> ([], []) | (x, y)::t -> let (l1, l2) = listapar2paralist t in (x :: l1, y :: l2);;
Możliwe jest definiowanie list nieskończonych. Należy jednak zdawać sobie sprawę, że listy są implementowane jako wskaźnikowe listy jednokierunkowe. Dlatego też lista nieskończona może mieć co najwyżej postać cyklu z "ogonkiem". Poniższy przykład pokazuje jak definiować listy nieskończone. Pokazuje on również, że nie tylko procedury mogą być obiektami definiowanymi rekurencyjnie.
Przykład [Definicje list nieskończonych]
let rec jedynki = 1::jedynki;; val jedynki : int list = [1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; ...] let rec cykl = 1 :: 0 :: -1 :: 0 :: cykl;; val cykl : int list = [1; 0; -1; 0; 1; 0; -1; 0; ...] [1;2;3] @ cykl;; - : int list = [1; 2; 3; 1; 0; -1; 0; 1; 0; -1; 0; ...] cykl @ [1;2;3];; Stack overflow during evaluation (looping recursion?).
Moduł List zawiera wiele poręcznych procedur operujących na listach. O modułach będziemy mówić dokładniej w jednym z wykładów. W tej chwili wystarczy nam wiedzieć, że aby korzystać z pojęć zdefiniowanych w module List, wystarczy wprowadzić polecenie open List;; lub przed każdą nazwą pojęcia dodać przedrostek postaci List. Moduł ten implementuje m.in. następujące operacje:
- length -- oblicza długość listy (uwaga: działa w czasie proporcjonalnym do długości listy),
- hd -- zwraca głowę (niepustej) listy,
- tl -- zwraca ogon (niepustej) listy,
- rev -- oblicza listę złożoną z tych samych elementów, ale w odwrotnej kolejności (uwaga: działa w czasie proporcjonalnym do długości listy),
- nth -- zwraca element z podanej pozycji na liście (głowa ma numer 0, uwaga: działa w czasie proporcjonalnym do numeru pozycji plus 1),
- append -- sklejanie list, działa tak jak @, ale nie jest zapisywane infiksowo, tylko tak, jak zwykła procedura.
open List;; length ["To"; "ci"; "dopiero"; "lista"];; - : int = 4 hd ["To"; "ci"; "dopiero"; "lista"];; - : string = "To" tl ["To"; "ci"; "dopiero"; "lista"];; - : string list = ["ci"; "dopiero"; "lista"] List.rev ["To"; "ci"; "dopiero"; "lista"];; - : string list = ["lista"; "dopiero"; "ci"; "To"] nth ["To"; "ci"; "dopiero"; "lista"] 2;; - : string = "dopiero" List.append [1; 2; 3] [4; 5];; - : int list = [1; 2; 3; 4; 5]
Deklaracje typów
Zwykle kompilator Ocamla sam jest w stanie wywnioskować jakiego typu są wyrażenia. Istnieje jednak możliwość nazywania typów i określania jakiego typu się spodziewamy. Wówczas kompilator sprawdza, czy dane wyrażenie faktycznie jest takiego typu, jak chcieliśmy, a zamiast pełnego rozwinięcia typu może używać podanej przez nas krótszej nazwy. Niektóre typy danych można wprowadzić tylko poprzez ich deklarację. Są to te typy, których deklaracje wprowadzają nowe konstruktory.
Możliwe jest też zdefiniowanie typów sparametryzowanych, tzn. takich, w których typy niektórych ich składowych są podawane jako parametry typu. Elementy struktury danych określone jako parametry mogą być dowolnego typu, ale wszystkie elementy określone przez ten sam parametr muszą być (w danej wartości) tego samego typu. (Uważny czytelnik zapewne domyśla się, że przykładem typu sparametryzowanego jest lista. Parametrem typu listowego jest typ elementów listy.) Parametry typu podajemy zwyczajowo przed nazwą typu sparametryzowanego, np. int list.
<jednostka kompilacji> ::= <deklaracja typu> <deklaracja typu> ::= type <identyfikator> = <typ> | type <parametr typowy> <identyfikator> = <typ> | type ( <parametr typowy> {, <parametr typowy>}* ) <identyfikator> = <typ> <typ> ::= <identyfikator> | <typ> <identyfikator> | ( <typ> {, <typ>}* ) <identyfikator> | <typ> { * <typ> }* | ( <typ> ) | ... <parametr typowy> ::= '<identyfikator>
Przykład [Deklaracje typów]
type p = int * float;; type p = int * float type 'a lista = 'a list;; type 'a lista = 'a list type ('a, 'b) para = 'a * 'b;; type ('a, 'b) para = 'a * 'b type 'a t = ('a, 'a) para * ('a list) list;; type 'a t = ('a, 'a) para * 'a list list
Podstawowym zastosowaniem deklaracji typów (oprócz wprowadzania typów, które wymagają zadeklarowania) jest definiowanie typów złożonych, które występują w interfejsach procedur. Wówczas można podać, iż oczekujemy, że dany argument powinien być określonego typu. W ogólności można dla dowolnego wzorca lub wyrażenia podać typ, jakiego oczekujemy.
<wyrażenie> ::= ( <wyrażenie> : <typ> ) <wzorzec> ::= ( <wzorzec> : <typ> )
Przykład [Specyfikacja typów]
type point = float * float;; type point = float * float type vector = point;; type vector = point let (p:point) = (2.0, 3.0);; val p : point = (2., 3.) let shift ((x, y): point) ((xo, yo): vector) = ((x +. xo, y +. yo) : point);; val shift : point -> vector -> point = <fun> shift p p;; - : point = (4., 6.)
Rekordy
Typy rekordowe są podobne do produktów kartezjańskich. Rekord odpowiada -tce, ale współrzędne (tzw. pola rekordu) są identyfikowane raczej po nazwie, a nie wg. pozycji w -tce. Typy rekordowe występują w wielu językach programowania -- w C i C++ są nazywane strukturami. Typy rekordowe wprowadza się deklarując typ postaci:
<typ> ::= { <identyfikator> : <typ> { ; <identyfikator> : <typ> }* }
Identyfikatory to nazwy pól rekordów. Konstruktor wartości rekordowych ma postać:
<wyrażenie> ::= { <identyfikator> = <wyrażenie> { ; <identyfikator> = <wyrażenie> }* } <wzorzec> ::= { <identyfikator> = <wzorzec> { ; <identyfikator> = <wzorzec> }* }
Używając takiego konstruktora do budowy rekordu musimy podać wartości wszystkich pól (w dowolnej kolejności). Natomiast używając go jako wzorca, możemy podać tylko interesujące nas pola. Do pojedynczych pól rekordów możemy się również odwoływać tak, jak w innych językach programowania, podając nazwę pola po kropce.
<wyrażenie> ::= <wyrażenie> . <identyfikator>
Przykład [Rekordy]
type ułamek = { licznik : int ; mianownik : int };; type ulamek = { licznik : int; mianownik : int; } let q = {licznik = 3; mianownik = 4};; val q : ulamek = {licznik = 3; mianownik = 4} let {licznik = a ; mianownik = b} = q;; val a : int = 3 val b : int = 4 let {licznik = c} = q;; val c : int = 3 q.licznik;; - : int = 3
Ze względu na przysłanianie nazw, należy unikać sytuacji, gdy dwa pola różnych typów rekordowych mają takie same nazwy. Wówczas będziemy mogli korzystać tylko z tego pola, które zostało zdefiniowane jako ostatnie.
Warianty
W Ocamlu istnieje również odpowiednik unii -- typy wariantowe, zwane też typami algebraicznymi. Deklarujemy je w następujący sposób:
<typ> ::= <wariant> { | <wariant> }* <wariant> ::= <Identyfikator> [ of <typ> ]
Przez <Identyfikator> oznaczamy identyfikatory rozpoczynające się wielką literą. Natomiast <identyfikator> oznacza identyfikatory rozpoczynające się małą literą.
Jak zobaczymy za chwilę, rozróżnienie to jest konieczne dla odróżnienia we wzorcach nazw konstruktorów od nazw wprowadzanych stałych.Każdy zadeklarowany wariant wprowadza konstruktor o podanej nazwie. Konstruktor ten może być bezparametrowy lub może mieć argumenty. Formalnie konstruktor taki może mieć co najwyżej jeden argument, ale zawsze może to być argument odpowiedniego typu produktowego. Konstruktorów typów wariantowych używamy w następujący sposób:
<wyrażenie> ::= <Identyfikator> [ <wyrażenie> ] <wzorzec> ::= <Identyfikator> [ <wzorzec> ]
Na typach wariantowych jest określona równość. Dwie wartości są równe, jeśli są wynikiem tego samego konstruktora, a jeżeli jest to konstruktor sparametryzowany, to dodatkowo parametry konstruktora muszą być sobie równe.
Przykład [Typy wariantowe]
type znak = Dodatni | Ujemny | Zero;; type znak = Dodatni | Ujemny | Zero let nieujemny x = match x with Ujemny -> false | _ -> true;; val nieujemny : znak -> bool = <fun> let ujemny x = x = Ujemny;; val ujemny : znak -> bool = <fun> type zespolone = Prostokatny of float * float | Biegunowy of float * float ;; type zespolone = Prostokatny of float * float | Biegunowy of float * float let modul = function Prostokatny (x, y) -> sqrt (square x +. square y) | Biegunowy (r, _) -> r;; val modul : zespolone -> float = <fun>
Deklaracje typów wariantowych mogą być rekurencyjne, co pozwala na konstruowanie drzew.
Przykład [Deklaracja drzew]
type drzewo = Puste | Wezel of int * drzewo * drzewo;; type drzewo = Puste | Wezel of int * drzewo * drzewo
Podobnie jak w przypadku list, możemy tworzyć "nieskończone" drzewa poprzez ich zacyklenie.
Przykład [Zacyklone drzewa]
let rec t = Wezel (42, t, Puste);; val t : drzewo = Wezel (42, Wezel (42, (...), Puste),Puste)
Deklaracje typów wariantowych mogą być sparametryzowane. Jest to szczególnie przydatne wówczas, gdy nie chcemy do końca określać typu składowych elementów.
Przykład [Sparametryzowane deklaracje typów]
type 'a lista = Pusta | Pelna of 'a * 'a lista;; type 'a lista = Pusta | Pelna of 'a * 'a lista Pelna (4, Pusta);; - : int lista = Pelna (4, Pusta) Pelna ("ala", Pelna("ula", Pusta));; - : string lista = Pelna ("ala", Pelna ("ula", Pusta)) Pelna (4, Pelna ("ula", Pusta));; error ...
Aliasy we wzorcach
Czasami, gdy używamy złożonych wzorców, chcemy zarówno uchwycić złożoną wartość, jak i jej elementy. Możemy to zrobić za pomocą konstrukcji as.
<wzorzec> ::= <wzorzec> as <identyfikator>
Dopasowywana wartość jest przypisywana identyfikatorowi po prawej stronie as oraz jest dopasowywana do wzorca po lewej stronie.
Przykład [Użycie as we wzorcach]
let (x, y) as para = (2, 3);; val x : int = 2 val y : int = 3 val para : int * int = (2, 3) let (h::t) as lista = [1; 2; 3; 4];; val h : int = 1 val t : int list = [2; 3; 4] val lista : int list = [1; 2; 3; 4]
Wyjątki
Czasami chcemy zaprogramować w postaci procedur funkcje częściowe. W jaki sposób możemy to zrobić? Jaki powinien być wynik dla punktów spoza dziedziny? Możemy użyć tu mechanizmu wyjątków. W języku istnieje specjalny typ wariantowy exn. Jest to nietypowy typ, gdyż można na bieżąco rozszerzać zestaw wariantów tworzących ten typ. Wartości tego typu niosą informację o wyjątkowych sytuacjach uniemożliwiających wykonanie obliczeń. Nowe warianty typu exn możemy deklarować w następujący sposób:
<jednostka kompilacji> ::= <deklaracja wyjątku> | ... <deklaracja wyjątku> ::= exception <wariant>
Z wyjątkami można robić dwie podstawowe rzeczy: podnosić i przechwytywać. Podniesienie wyjątku to wyrażenie, którego obliczenie "nie udaje się", a porażce tej towarzyszy wartość podnoszonego wyjątku (typu exn). Jeśli obliczenie dowolnego podwyrażenia nie udaje się na skutek podniesienia wyjątku, to tak samo nie udaje się obliczenie całego wyrażenia. Tak więc wyjątek to rodzaj propagującego się "błędu w obliczeniach". Podniesienie wyjątku ma następującą postać, przy czym argument operacji raise musi być typu exn:
<wyrażenie> ::= raise <wyrażenie>
Przechwycenie wyjątku to konstrukcja odwrotna do jego podniesienia. Możemy spróbować obliczyć wartość danego wyrażenia, licząc się z możliwością podniesienia wyjątku. Jeśli w danym wyrażeniu zostanie podniesiony wyjątek, to możemy go przechwycić i podać, jaka powinna być wartość całego wyrażenia w takim przypadku.
<wyrażenie> ::= try <wyrażenie> with { <wzorzec> -> <wyrażenie> | }* <wzorzec> -> <wyrażenie>
Przykład [Wyjątki]
exception Dzielenie_przez_zero of float;; exception Dzielenie_przez_zero of float let dziel x y = if y = 0.0 then raise (Dzielenie_przez_zero y) else x /. y;; val dziel : float -> float -> float = <fun> let odwrotnosc x = try dziel 1.0 x with Dzielenie_przez_zero _ -> 0.0;; val odwrotnosc : float -> float = <fun> odwrotnosc 42.;; - : float = 0.0238095238095238082 odwrotnosc 0.0;; - : float = 0.
Standardowo zdefiniowany jest wyjątek Failure of string oraz procedura:
let failwith s = raise (Failure s);;
Pamiętajmy, że należy odróżniać wartości typu exn od podniesienia wyjątku.
Dzielenie_przez_zero 4.5;; - : exn = Dzielenie_przez_zero 4.5 raise (Dzielenie_przez_zero 4.5);; Exception: Dzielenie_przez_zero 4.5.
W niektórych źródłach wyjątki są opisane jako konstrukcja imperatywna. Proszę jednak zwrócić uwagę, że nie odwołują się one do takich pojęć, jak zmienne, obiekty, stany, czy przypisanie.
Dlatego też wyjątki są tu przedstawione jako mechanizm funkcyjny.Przykłady ciekawych struktur danych
Kolejki FIFO
Kolejka FIFO (ang. first in, first out) to taka kolejka, do której możemy wkładać elementy na koniec i wyjmować elementy z początku. Dodatkowo możemy podglądać jaki element jest pierwszy w kolejce. Kolejkę fifo reprezentujemy jako trójkę:
- listę, którą będziemy nazywać "przednią",
- listę, którą będziemy nazywać "tylną" i
- liczba elementów w kolejce.
Lista przednia
, lista tylna
reprezentują kolejkę zawierającą
elementów:
. Tak więc każdy element w kolejce występuje na jednej z list, wszystkie elementy z listy przedniej poprzedzają elementy z listy tylnej, elementy na liście przedniej występują zgodnie z kolejnością w kolejce, a na liście tylnej w odwrotnej kolejności. Dodatkowo zakładamy, że jeżeli lista przednia jest pusta, to tylna też musi być pusta. Wyjmując element z kolejki, wyjmujemy go z listy przedniej, a wkładając, wkładamy do tylnej. Gdy zabraknie elementów do wyjmowania, "przelewamy" elementy z listy tylnej do przedniej, odwracając ich kolejność na liście.
type 'a kolejka = {front: 'a list; back: 'a list; size: int};; type 'a kolejka = { front : 'a list; back : 'a list; size : int; } let empty_queue = {front=[]; back=[]; size=0};; val empty_queue : 'a kolejka = {front = []; back = []; size = 0} let size q = q.size;; val size : 'a kolejka -> int = <fun> let balance q = match q with {front=[]; back=[]} -> q | {front=[]; back=b; size=s} -> {front=List.rev b; back=[]; size=s} | _ -> q;; val balance : 'a kolejka -> 'a kolejka = <fun> let put {front=f; back=b; size=n} x = balance {front=f; back=x::b; size=n+1};; val put : 'a kolejka -> 'a -> 'a kolejka = <fun> let first q = match q with {front=[]} -> failwith "Pusta kolejka" | {front=x::_} -> x;; val first : 'a kolejka -> 'a = <fun> let remove q = match q with {front=_::f} -> balance {front=f; back=q.back; size=q.size-1} | _ -> empty_queue;; val remove : 'a kolejka -> 'a kolejka = <fun>
Operacja balance zapewnia, że jeżeli kolejka nie jest pusta, to jej lista przednia nie jest pusta. Zanalizujmy koszt operacji na kolejkach (zakładając, że pamiętamy jedną kolejkę na raz). Pomijając koszt procedury balance, wszystkie operacje na kolejce mają koszt stały. Pokażemy, że koszt zamortyzowany procedury balance jest też stały.
Jako funkcję amortyzacji przyjmujemy długość listy tylnej. Dla pustej kolejki funkcja amortyzacji jest równa 0. Procedura put ma koszt rzeczywisty stały i powoduje zwiększenie funkcji amortyzacji o 1, co daje stały koszt zamortyzowany. Procedura remove może mieć koszt rzeczywisty liniowy ze względu na odwracanie elementów z listy tylnej. Jednak równocześnie zmniejsza ona funkcję amortyzacji do 0, czyli o tyle, ile trwa odwracanie listy. Tak więc koszt zamortyzowany procedury remove jest stały.
Zbiory skończone jako drzewa BST
Poniżej przedstawiamy implementację drzew BST. Typ reprezentujący drzewa binarne możemy zadeklarować nastepująco:
type 'a finset = Empty | Node of ('a * 'a finset * 'a finset);; type 'a finset = Empty | Node of ('a * 'a finset * 'a finset)
Zwróćmy uwagę, że jest to sparametryzowana deklaracja rekurencyjna. Czymkolwiek by był parametr `a, wszystkie węzły drzewa muszą zawierać wartości tego samego typu.
(* Wyjątek podnoszony, gdy badamy wartość spoza dziedziny. *) exception Undefined;; exception Undefined (* Pusty zbiór to puste drzewo *) let empty_finset = Empty;; val empty_finset : 'a finset = Empty (* Znajdź poddrzewo o zadanym kluczu *) (* (jeśli nie ma, to wynikiem jest puste drzewo). *) let rec find s e = match s with Empty -> Empty | Node (k, l, r) -> if e = k then s else if e < k then find l e else find r e;; val find : 'a finset -> 'a -> 'a finset = <fun> (* Predykat charakterystyczny zbioru. *) let member s e = not (find s e = Empty);; val member : 'a finset -> 'a -> bool = <fun> (* Wstawienie elementu do zbioru *) let rec insert s e = match s with Empty -> Node (e, Empty, Empty) | Node (k, l, r) -> if e = k then s else if e < k then Node (k, insert l e, r) else Node (k, l, insert r e);; val insert : 'a finset -> 'a -> 'a finset = <fun> (* Najmniejszy element *) let rec min s = match s with Node (e, Empty, _) -> e | Node (_, l, _) -> min l | Empty -> raise Undefined;; val min : 'a finset -> 'a = <fun> (* Największy element *) let rec max s = match s with Node (e, _, Empty) -> e | Node (_, _, r) -> max r | Empty -> raise Undefined;; val max : 'a finset -> 'a = <fun> (* Usunięcie elementu z listy *) let rec remove s e = match s with Empty -> s | Node (k, l, r) -> if e = k then match (l, r) with (Empty, Empty) -> Empty | (Empty, _) -> let m = min r in Node (m, l, remove r m) | _ -> let m = max l in Node (m, remove l m, r) else if e < k then Node (k, remove l e, r) else Node (k, l, remove r e);; val remove : 'a finset -> 'a -> 'a finset = <fun>