Programowanie funkcyjne/Struktury danych: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Kubica (dyskusja | edycje)
m Zastępowanie tekstu – „<math> ” na „<math>”
 
(Nie pokazano 45 wersji utworzonych przez 4 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 tym wykładzie.  
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, środowisko przyporządkowuje każdej nazwie stałej jej wartość, która jest określonego typu.  
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, operacje arytmetyczne na liczbach całkowitych i zmiennopozycyjnych muszą mieć różne nazwy,  
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, to szczególnego rodzaju operacje tworzące wartości określonych typów złożonych.  
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.
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 żłożonych,  
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.


Narazie nie znamy jeszcze żadnych konstruktoró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> |
<definicja> ::= <u>let</u> [<u>rec</u>] <identyfikator> {<wzorzec>}<sup>+</sup> <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> |
<wyrażenie> ::= <u>function</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, to definicje stałych.  
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, nazwa procedury musi być identyfikatorem, ale parametry formalne mogą być wzorcami.  
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''
&nbsp;
 
  f 6 9;;
  f 6 9;;
  - : int = 42
  ''- : int = 42''
&nbsp;
 
  '''let''' p x _ = x + 1;;
  '''let''' p x _ = x + 1;;
  ''val p : int -> 'a -> int = <fun>''
  ''val p : int -> 'a -> int = <fun>''
&nbsp;
 
  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 =
&nbsp;&nbsp;'''function''' 1 -> 1 | x -> x * silnia (x - 1);; <br/>
  '''function''' 1 -> 1 | x -> x * silnia (x - 1);;  
  '''let''' '''rec''' silnia x =
  '''let''' '''rec''' silnia x =
&nbsp;&nbsp;'''match''' x '''with'''
  '''match''' x '''with'''
&nbsp;&nbsp;&nbsp;&nbsp;1 -> 1 |
    1 -> 1 |
&nbsp;&nbsp;&nbsp;&nbsp;x -> x * silnia (x-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.  
<p>
Na wartościach produktów kartezjańskich jest określona równość.
Dwie <math>n</math>-tki są są równe, jeśli ich odpowiadające sobie współrzędne są równe.
</p>


<p align="justify"> 


Na wartościach produktów kartezjańskich jest określona równość. Dwie <math> n</math>-tki są są równe, jeśli ich odpowiadające sobie współrzędne są równe. Istnieje "jedynka" produktu kartezjańskiego (odpowiednik typu <tt>void</tt>), typ <tt>unit</tt>. Jedyną wartością tego typu jest <tt>()</tt>.
<wyrażenie> ::= <u>(</u> [ <wyrażenie> { <u>,</u> <wyrażenie> }<sup>*</sup> ] <u>)</u>
</p>
<wzorzec>  ::= <u>(</u> [ <wzorzec> { <u>,</u> <wzorzec> }<sup>*</sup> ] <u>)</u>


<wyrażenie> ::= <u>(</u> [ <wyrażenie> { <u>,</u> <wyrażenie> }* ] <u>)</u>
{{przyklad|[Produkty kartezjańskie]||}}
<wzorzec>  ::= <u>(</u> [ <wzorzec> { <u>,</u> <wzorzec> }* ] <u>)</u>


  (1, 2, 3);;
  (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");;
  '''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);;
  '''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"> 
uwaga||uwaga_6|Wartości <tt>(1, 2, 3)</tt>, <tt>((1, 2), 3)</tt> i <tt>(1, (2, 3))</tt> są różnych typów.
Produkt kartezjański nie jest łączny, tzn. poniższe wyrażenia są różnych typów:
}}
</p>
 
(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)''
 
<p align="justify">
Jednoelementowe <math>n</math>-tki wartości typu <math>t</math> to po prostu wartości typu <math>t</math>,
<math>(x) = x</math>.
Istnieje "jedynka" produktu kartezjańskiego (odpowiednik typu <tt>void</tt>), typ <tt>unit</tt>.
Jedyną wartością tego typu jest <tt>()</tt>.
Jednak <tt>t * unit</tt> to inny typ niż <tt>t</tt>.
</p>


==Listy==
==Listy==
<p align="justify">
<p align="justify">
Listy to skończone ciągi elementów tego samego typu. Konstruktory: <math> h::t</math> i <math> [x_1; x_2; \dots; x_n]</math>,
Listy to ciągi elementów tego samego typu (skończone lub nieskończone, ale od pewnego miejsca okresowe).  
przy czym ten ostatni jest równoważny <math> x_1 :: (x_2 :: \dots :: (x_n :: [])\dots)</math>.
Typ listy elementów typu <tt>t</tt> oznaczamy przez <tt>t list</tt>.
Dostępna jest też operacja sklejania list <tt>@</tt>. Operacje <tt>::</tt> i <tt>[]</tt> obliczają się w czasie stałym, natomiast <tt>@</tt> w czasie proporcjonalnym do długości pierwszegoargumentu. 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.  
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.
</p>
</p>
<p align="justify">
Typowi listowemu towarzyszą dwa konstruktory:
<tt> h::t</tt> i <tt>[]</tt>.
<tt>[]</tt> to konstruktor listy pustej, a <tt> h::t</tt> tworzy listę, której głową jest <tt>h</tt>,
a ogonem jest <tt>t</tt>.
</p>
<p align="justify">
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]</math>.
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.
</p>
<p align="justify">
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.
</p>


{{przyklad|[Listy]||}}
{{przyklad|[Listy]||}}


[];;
''- : 'a 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];;
''- : 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"]''
&nbsp;&nbsp;
 
  '''let''' '''rec''' length l =
  '''let''' '''rec''' length l =
&nbsp;&nbsp;'''match''' l '''with'''
  '''match''' l '''with'''
&nbsp;&nbsp;&nbsp;&nbsp;[]&nbsp;&nbsp; -> 0 |
    []   -> 0 |
&nbsp;&nbsp;&nbsp;&nbsp;_::t -> 1 + length t;;
    _::t -> 1 + length t;;
&nbsp;&nbsp;
 
  '''let''' '''rec''' sumuj l =
  '''let''' '''rec''' sumuj l =
&nbsp;&nbsp;'''match''' l '''with'''
  '''match''' l '''with'''
&nbsp;&nbsp;&nbsp;&nbsp;[]&nbsp;&nbsp; -> 0 |
    []   -> 0 |
&nbsp;&nbsp;&nbsp;&nbsp;h::t -> h + sumuj t;;
    h::t -> h + sumuj t;;
&nbsp;&nbsp;
 
  '''let''' '''rec''' listapar2paralist =
  '''let''' '''rec''' listapar2paralist =
&nbsp;&nbsp;'''function'''
  '''function'''
&nbsp;&nbsp;&nbsp;&nbsp;[]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; -> ([], []) |
    []       -> ([], []) |
&nbsp;&nbsp;&nbsp;&nbsp;(x, y)::t ->  
    (x, y)::t ->  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''let''' (l1, l2) = listapar2paralist t
      '''let''' (l1, l2) = listapar2paralist t
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''in''' (x :: l1, y :: l2);;
      '''in''' (x :: l1, y :: l2);;
   
 
Moduł <tt>List</tt> zawiera wiele poręcznych procedur operujących nalistach.
<p align="justify">
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.
</p>
 
{{przyklad|[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?).''
 
<p align="justify">
Moduł <tt>List</tt> 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 <tt>List</tt>,
wystarczy wprowadzić polecenie <tt>open List;;</tt> lub przed każdą nazwą pojęcia dodać przedrostek
postaci <tt>List.</tt>
Moduł ten implementuje m.in. następujące operacje:
</p>
 
* <tt>length</tt> -- oblicza długość listy (uwaga: działa w czasie proporcjonalnym do długości listy),
* <tt>hd</tt> -- zwraca głowę (niepustej) listy,
* <tt>tl</tt> -- zwraca ogon (niepustej) listy,
* <tt>rev</tt> -- 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),
* <tt>nth</tt> -- zwraca element z podanej pozycji na liście (głowa ma numer 0, uwaga: działa w czasie proporcjonalnym do numeru pozycji plus 1),
* <tt>append</tt> -- sklejanie list, działa tak jak <tt>@</tt>, ale nie jest zapisywane infiksowo, tylko tak, jak zwykła procedura.


  open List;;
  open List;;
  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"]''
  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"''
  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]''


==Deklaracje typów==
==Deklaracje typów==
<p align="justify">
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.
</p>
<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  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. <tt>int list</tt>.
</p>
<jednostka kompilacji> ::= <deklaracja typu>
<deklaracja typu>      ::= <u>type</u> <identyfikator> <u>=</u> <typ> | <u>type</u> <parametr typowy> <identyfikator> <u>=</u> <typ> |
                            <u>type</u> <u>(</u> <parametr typowy> {<u>,</u> <parametr typowy>}<sup>*</sup> <u>)</u> <identyfikator> <u>=</u> <typ>
<typ>                  ::= <identyfikator> | <typ> <identyfikator> | <u>(</u> <typ> {<u>,</u> <typ>}<sup>*</sup> <u>)</u> <identyfikator> |
                            <typ> { <u>*</u> <typ> }<sup>*</sup> | <u>(</u> <typ> <u>)</u> | ...
<parametr typowy>      ::= <u>'</u><identyfikator>
{{przyklad|[Deklaracje typów]||}}
'''type''' p = int * float;;
''type p = int * float''
&nbsp;
'''type''' 'a lista = 'a list;;
''type 'a lista = 'a list''
&nbsp;
'''type''' ('a, 'b) para = 'a * 'b;;
''type ('a, 'b) para = 'a * 'b''
&nbsp;
'''type''' 'a t = ('a, 'a) para * ('a list) list;;
''type 'a t = ('a, 'a) para * 'a list list''
<p align="justify">
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.
</p>


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 nazwy. Niektóre typy danych, można wprowadzić tylko poprzez ich deklarację. Są to te typy, których deklaracje wprowadzają nowe konstruktory.
<wyrażenie> ::= <u>(</u> <wyrażenie> <u>:</u> <typ> <u>)</u>
<wzorzec>  ::= <u>(</u> <wzorzec> <u>:</u> <typ> <u>)</u>


{{przyklad|[Specyfikacja typów]||}}


  <jednostka kompilacji> ::= <deklaracja typu>
  '''type''' point = float * float;;
  <deklaracja typu>      ::= <u>type</u> <identyfikator> <u>=</u> <typ>  
''type point = float * float''
  <typ>                  ::= <u>int</u> | <u>float</u> | <u>char</u> | <u>string</u> | <u>unit</u> |
&nbsp;
                            <identyfikator> | <typ> <u>list</u> |
'''type''' vector = point;;
                            <typ> { <u>*</u> <typ> }* | <u>(</u> <typ> <u>)</u> | ...
''type vector = point''
&nbsp;
'''let''' (p:point) = (2.0, 3.0);;
''val p : point = (2., 3.)''
&nbsp;
  '''let''' shift ((x, y): point) ((xo, yo): vector) =  
  ((x +. xo, y +. yo) : point);;
''val shift : point -> vector -> point = <fun>''
&nbsp;
shift p p;;
  ''- : point = (4., 6.)''


==Rekordy==
==Rekordy==
<p align="justify">
Typy rekordowe są podobne do produktów kartezjańskich.
Rekord odpowiada <math>n</math>-tce, ale współrzędne (tzw. pola rekordu) są identyfikowane raczej po nazwie,
a nie wg. pozycji w <math>n</math>-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:
Typy rekordowe wprowadza się deklarując typ postaci:
</p>


  <typ> ::= <u>{</u> <identyfikator> <u>:</u> <typ> { <u>;</u> <identyfikator> <u>:</u> <typ> }* <u>}</u>
  <typ> ::= <u>{</u> <identyfikator> <u>:</u> <typ> { <u>;</u> <identyfikator> <u>:</u> <typ> }* <u>}</u>


<p align="justify">
Identyfikatory to nazwy pól rekordów.
Konstruktor wartości rekordowych ma postać:
Konstruktor wartości rekordowych ma postać:
</p>


  <wyrażenie> ::= <u>{</u> <identyfikator> <u>=</u> <wyrażenie>  
  <wyrażenie> ::= <u>{</u> <identyfikator> <u>=</u> <wyrażenie>  
Linia 279: Linia 462:
                 { <u>;</u> <identyfikator> <u>=</u> <wzorzec> }* <u>}</u>  
                 { <u>;</u> <identyfikator> <u>=</u> <wzorzec> }* <u>}</u>  


Używając takiego konstruktora do budowy wartości musimy podać wartości wszystkich pól rekordu (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
<p align="justify">
w Pascalu, podając nazwę pola po kropce.
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.
</p>


  <wyrażenie> ::= <wyrażenie> <u>.</u> <identyfikator>
  <wyrażenie> ::= <wyrażenie> <u>.</u> <identyfikator>
{{przyklad|[Rekordy]||}}


  '''type''' ułamek = { licznik : int ; mianownik : int };;
  '''type''' ułamek = { licznik : int ; mianownik : int };;
  '''''type''' ulamek = { licznik : int; mianownik : int; }''
  ''type ulamek = { licznik : int; mianownik : int; }''
  '''let''' q = {licznik = 3; mianownik = 4\};;
  '''''val''' q : ulamek = {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 a : int = 3''
  '''''val''' b : int = 4''
  ''val b : int = 4''
  '''let''' {licznik = c} = q;;
  '''let''' {licznik = c} = q;;
  '''''val''' c : int = 3''
  ''val c : int = 3''
  q.licznik;;
  q.licznik;;
  ''- : int = 3''
  ''- : int = 3''


{{
{{uwaga||uwaga_7|
uwaga||uwaga_7|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, które zostało zdefiniowane jako ostatnie.
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==
==Warianty==
 
<p align="justify">
W Ocamlu istnieje również odpowiednik unii --- typy wariantowe, zwane też algebraicznymi. Deklarujemy je w następujący sposób:
W Ocamlu istnieje również odpowiednik unii -- typy wariantowe, zwane też typami algebraicznymi.  
Deklarujemy je w następujący sposób:
</p>
        
        
  <typ>    ::= <wariant> { <u>|</u> <wariant> }*  
  <typ>    ::= <wariant> { <u>|</u> <wariant> }*  
  <wariant> ::= <Identyfikator> [ <u>of</u> <typ> ]
  <wariant> ::= <Identyfikator> [ <u>of</u> <typ> ]


Każdy zadeklarowany wariant wprowadza konstruktor o podanej nazwie. Konstruktor ten może być bezparametrowy, lub może mieć argumenty.  
{{uwaga||uwaga_8|
Przez <tt><Identyfikator></tt> oznaczamy identyfikatory rozpoczynające się wielką literą.  
Natomiast <tt><identyfikator></tt> 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.}}


{{
<p align="justify">
uwaga||uwaga_8|<tt><Identyfikator></tt> to identyfikator rozpoczynający się wielką literą.
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.  
W Ocamlu identyfikatory rozpoczynające się wielką literą są nazwami konstruktorów, a małą nazwami wartości. Jest to konieczne ze względu na składnię wzorców. 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:
</p>
        
        
  <wyrażenie> ::= <Identyfikator> [ <wyrażenie> ]  
  <wyrażenie> ::= <Identyfikator> [ <wyrażenie> ]  
  <wzorzec>  ::= <Identyfikator> [ <wzorzec> ]
  <wzorzec>  ::= <Identyfikator> [ <wzorzec> ]


Deklaracje typów wariantowych mogą być rekurencyjne, co pozwala na konstruowanie drzew. Przypomina to zdefiniowanie w Pascalu typu wskaźnikowego do typu, który jest definiowany dalej i w którym można korzystać z danego typu wskaźnikowego.  
<p align="justify">
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.
</p>


{{przyklad|[Typy wariantowe]||}}
{{przyklad|[Typy wariantowe]||}}


  '''type''' znak = Dodatni | Ujemny | Zero;;
  '''type''' znak = Dodatni | Ujemny | Zero;;
''type znak = Dodatni | Ujemny | Zero''
  &nbsp;&nbsp;
  &nbsp;&nbsp;
  '''let''' nieujemny x =
  '''let''' nieujemny x =
Linia 328: Linia 533:
  &nbsp;&nbsp;&nbsp;&nbsp;Ujemny -> false |
  &nbsp;&nbsp;&nbsp;&nbsp;Ujemny -> false |
  &nbsp;&nbsp;&nbsp;&nbsp;_&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; -> true;;
  &nbsp;&nbsp;&nbsp;&nbsp;_&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; -> true;;
''val nieujemny : znak -> bool = <fun>''
  &nbsp;&nbsp;
  &nbsp;&nbsp;
'''let''' ujemny x =
  x = Ujemny;;
''val ujemny : znak -> bool = <fun>''
&nbsp;
  '''type''' zespolone =
  '''type''' zespolone =
  &nbsp;&nbsp;Prostokątny '''of''' float * float |
  &nbsp;&nbsp;Prostokatny '''of''' float * float |
  &nbsp;&nbsp;Biegunowy '''of''' float * float ;;
  &nbsp;&nbsp;Biegunowy '''of''' float * float ;;
''type zespolone = Prostokatny of float * float | Biegunowy of float * float''
  &nbsp;&nbsp;
  &nbsp;&nbsp;
  '''let''' moduł =
  '''let''' modul =
  &nbsp;&nbsp;'''function'''
  &nbsp;&nbsp;'''function'''
  &nbsp;&nbsp;&nbsp;&nbsp;Prostokątny (x, y) -> sqrt (square x +. square y) |
  &nbsp;&nbsp;&nbsp;&nbsp;Prostokatny (x, y) -> sqrt (square x +. square y) |
  &nbsp;&nbsp;&nbsp;&nbsp;Biegunowy (r, _) -> r;;
  &nbsp;&nbsp;&nbsp;&nbsp;Biegunowy (r, _)   -> r;;
  &nbsp;&nbsp;
  ''val modul : zespolone -> float = <fun>''
 
Deklaracje typów wariantowych mogą być rekurencyjne, co pozwala na konstruowanie drzew.
 
{{przyklad|[Deklaracja drzew]||}}
 
  '''type''' drzewo =
  '''type''' drzewo =
  &nbsp;&nbsp;Puste |
  &nbsp;&nbsp;Puste |
  &nbsp;&nbsp;Wezel '''of''' int * drzewo * drzewo;;
  &nbsp;&nbsp;Wezel '''of''' int * drzewo * drzewo;;
''type drzewo = Puste | Wezel of int * drzewo * drzewo''


Deklaracje typów mogą być sparametryzowane. Jest to szczególnie przydatne wówczas, gdy nie chcemy do końca określać typu składowych elementów. Parametry typowe zapisujemy poprzedzając je apostrofem.
<p align="justify">
Podobnie jak w przypadku list, możemy tworzyć "nieskończone" drzewa poprzez ich zacyklenie.
</p>
 
{{przyklad|[Zacyklone drzewa]||}}
 
'''let rec''' t = Wezel (42, t, Puste);;
''val t : drzewo = Wezel (42, Wezel (42, (...), Puste),Puste)''
 
<p align="justify">
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.  
</p>


{{przyklad|[Sparametryzowane deklaracje typów]||}}
{{przyklad|[Sparametryzowane deklaracje typów]||}}


  '''type''' 'a lista = Pusta | Pelna '''of''' 'a * 'a lista;;
  '''type''' 'a lista = Pusta | Pelna '''of''' 'a * 'a lista;;
''type 'a lista = Pusta | Pelna of 'a * 'a lista''
&nbsp;
  Pelna (4, Pusta);;
  Pelna (4, Pusta);;
  ''- : int lista = Pelna (4, Pusta)''
  ''- : int lista = Pelna (4, Pusta)''
&nbsp;
  Pelna ("ala", Pelna("ula", Pusta));;
  Pelna ("ala", Pelna("ula", Pusta));;
  ''- : string lista = Pelna ("ala", Pelna ("ula", Pusta))''
  ''- : string lista = Pelna ("ala", Pelna ("ula", Pusta))''
&nbsp;
  Pelna (4, Pelna ("ula", Pusta));;
  Pelna (4, Pelna ("ula", Pusta));;
  ''error ...''
  ''error ...''
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 odpowiadające sobie parametry konstruktora muszą być sobie równe.


==Aliasy we wzorcach==
==Aliasy we wzorcach==
Czasami, gdy używamy złożonych wzorców, chcemy równocześnie
<p align="justify">
uchwycić złożoną wartość, jak i jej elementy. Możemy to zrobić za pomocą konstrukcji <tt>as</tt>.       
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 <tt>as</tt>.       
</p>


  <wzorzec> ::= <wzorzec> <u>as</u> <identyfikator>       
  <wzorzec> ::= <wzorzec> <u>as</u> <identyfikator>       
<p align="justify">
Dopasowywana wartość jest przypisywana identyfikatorowi po prawej stronie <tt>as</tt> oraz jest dopasowywana do wzorca po lewej stronie.
</p>
{{przyklad|[Użycie <tt>as</tt> we wzorcach]||}}


  '''let''' (x, y) '''as''' para = (2, 3);;
  '''let''' (x, y) '''as''' para = (2, 3);;
  '''''val''' x : int = 2''
  ''val x : int = 2''
  '''''val''' y : int = 3''
  ''val y : int = 3''
  '''''val''' para : int * int = (2, 3)''
  ''val para : int * int = (2, 3)''
  &nbsp;&nbsp;
  &nbsp;
  '''let''' (h::t) '''as''' lista = [1; 2; 3; 4];;
  '''let''' (h::t) '''as''' lista = [1; 2; 3; 4];;
  '''''val''' h : int = 1''
  ''val h : int = 1''
  '''''val''' t : int list = [2; 3; 4]''
  ''val t : int list = [2; 3; 4]''
  '''''val''' lista : int list = [1; 2; 3; 4]''
  ''val lista : int list = [1; 2; 3; 4]''


==Wyjątki==
==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 <tt>exn</tt>.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 <tt>exn</tt> możemy deklarować w następującysposób:
<p align="justify">
     
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 <tt>exn</tt>.
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 <tt>exn</tt> możemy deklarować w następujący sposób:
</p>
 
  <jednostka kompilacji> ::= <deklaracja wyjątku> | ...
  <jednostka kompilacji> ::= <deklaracja wyjątku> | ...
  <deklaracja wyjątku>  ::= <u>exception</u> <wariant>       
  <deklaracja wyjątku>  ::= <u>exception</u> <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ść podnoszonegowyjątku (typu <tt>exn</tt>).Jeśli obliczenie dowolnego podwyrażenia nie daje 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 <tt>raise</tt> musi być typu <tt>exn</tt>:       
<p align="justify">
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 <tt>exn</tt>).
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 <tt>raise</tt> musi być typu <tt>exn</tt>:       
</p>


  <wyrażenie> ::= <u>raise</u> <wyrażenie>       
  <wyrażenie> ::= <u>raise</u> <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.
<p align="justify">
     
Przechwycenie wyjątku to konstrukcja odwrotna do jego podniesienia.  
<wyrażenie> ::= <u>try</u> <wyrażenie> <u>with</u> { <wzorzec> <u>-</u> } <wyrażenie> <u>|</u> }*
Możemy spróbować obliczyć wartość danego wyrażenia, licząc się z możliwością podniesienia wyjątku.  
                <wzorzec> <u>-></u> <wyrażenie>  
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.
</p>
        
        
<wyrażenie> ::= <u>try</u> <wyrażenie> <u>with</u> { <wzorzec> <u>-></u> <wyrażenie> <u>|</u> }<sup>*</sup> <wzorzec> <u>-></u> <wyrażenie>
{{przyklad|[Wyjątki]||}}
{{przyklad|[Wyjątki]||}}


  exception Dzielenie_przez_zero of float;;
  '''exception''' Dzielenie_przez_zero '''of''' float;;
''exception Dzielenie_przez_zero of float''
&nbsp;
  '''let''' dziel x y =
  '''let''' dziel x y =
  &nbsp;&nbsp;'''if''' y = 0.0 '''then'''
  &nbsp;&nbsp;'''if''' y = 0.0 '''then'''
  &nbsp;&nbsp;&nbsp;&nbsp;'''raise''' (Dzielenie_przez_zero y)
  &nbsp;&nbsp;&nbsp;&nbsp;'''raise''' (Dzielenie_przez_zero y)
  &nbsp;&nbsp;'''else''' x /. y;;
  &nbsp;&nbsp;'''else''' x /. y;;
''val dziel : float -> float -> float = <fun>''
&nbsp;
  '''let''' odwrotnosc x =
  '''let''' odwrotnosc x =
  &nbsp;&nbsp;'''try'''
  &nbsp;&nbsp;'''try'''
Linia 399: Linia 665:
  &nbsp;&nbsp;'''with'''
  &nbsp;&nbsp;'''with'''
  &nbsp;&nbsp;&nbsp;&nbsp;Dzielenie_przez_zero _ -> 0.0;;
  &nbsp;&nbsp;&nbsp;&nbsp;Dzielenie_przez_zero _ -> 0.0;;
   
  ''val odwrotnosc : float -> float = <fun>''
&nbsp;
odwrotnosc 42.;;
''- : float = 0.0238095238095238082''
&nbsp;
odwrotnosc 0.0;;
''- : float = 0.''
 
<p align="justify">
Standardowo zdefiniowany jest wyjątek <tt>Failure of string</tt> oraz procedura:
Standardowo zdefiniowany jest wyjątek <tt>Failure of string</tt> oraz procedura:
</p>


  '''let''' failwith s =
  '''let''' failwith s =
  &nbsp;&nbsp;'''raise''' (Failure s);;
  &nbsp;&nbsp;'''raise''' (Failure s);;


 
<p align="justify">
Należy odróżniać wartości typu <tt>exn</tt> od podniesienia wyjątku.  
Pamiętajmy, że należy odróżniać wartości typu <tt>exn</tt> od podniesienia wyjątku.  
</p>


  Dzielenie_przez_zero 4.5;;
  Dzielenie_przez_zero 4.5;;
  ''- : exn = Dzielenie_przez_zero 4.5''
  ''- : exn = Dzielenie_przez_zero 4.5''
&nbsp;
  '''raise''' (Dzielenie_przez_zero 4.5);;
  '''raise''' (Dzielenie_przez_zero 4.5);;
  ''Exception: Dzielenie_przez_zero 4.5.''
  ''Exception: Dzielenie_przez_zero 4.5.''


{{
{{uwaga||uwaga_9|
uwaga||uwaga_9|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. Uważam więc, że wyjątki, tak jak one zrealizowane w Ocamlu, są mechanizmem funkcyjnym.
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==
==Przykłady ciekawych struktur danych==
===Kolejki fifo===
===Kolejki FIFO===
[[[Uzupełnić o całkowicie funkcyjne kolejki.]]]
<p align="justify">
Kolejkę fifo reprezentujemy jako trójkę: listę przednią, listę tylną i rozmiar. Trójka postaci <math> ([x_1; \dots; x_k], [y_1; \dots; y_l], k+l)</math> reprezentuje kolejkę <math> x_1, \dots, x_k, y_l, \dots, y_1</math>. Dodatkowo zakładamy, że jeżeli pierwsza lista jest pusta, to druga też musi być pusta. Elementy wyjmujemy z pierwszej połówki, a wkładamy do drugiej. Gdy zabraknie elementów do wyjmowania, to "przelewamy" elementy z jednej połówki do drugiej, odwracając ich kolejność.  
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 <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.
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.  
</p>
 
  '''type''' 'a kolejka = {front: 'a list; back: 'a list; size: int};;
  '''type''' 'a kolejka = {front: 'a list; back: 'a list; size: int};;
''type 'a kolejka = { front : 'a list; back : 'a list; size : int; }''
  &nbsp;&nbsp;
  &nbsp;&nbsp;
  '''let''' empty_queue = {front=[]; back=[]; size=0};;
  '''let''' empty_queue = {front=[]; back=[]; size=0};;
''val empty_queue : 'a kolejka = {front = []; back = []; size = 0}''
  &nbsp;&nbsp;
  &nbsp;&nbsp;
  '''let''' size q = q.size;;
  '''let''' size q = q.size;;
''val size : 'a kolejka -> int = <fun>''
  &nbsp;&nbsp;
  &nbsp;&nbsp;
  '''let''' balance q =  
  '''let''' balance q =  
  &nbsp;&nbsp;'''match''' q '''with'''
  &nbsp;&nbsp;'''match''' q '''with'''
  &nbsp;&nbsp;&nbsp;&nbsp;{front=[]; back=[]}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-> q |
  &nbsp;&nbsp;&nbsp;&nbsp;{front=[]; back=[]}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-> q |
  &nbsp;&nbsp;&nbsp;&nbsp;{front=[]; back=b; size=s} -> {front=rev b; back=[]; size=s} |
  &nbsp;&nbsp;&nbsp;&nbsp;{front=[]; back=b; size=s} -> {front=List.rev b; back=[]; size=s} |  
  &nbsp;&nbsp;&nbsp;&nbsp;_&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-> q;;
  &nbsp;&nbsp;&nbsp;&nbsp;_&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-> q;;
''val balance : 'a kolejka -> 'a kolejka = <fun>''
  &nbsp;&nbsp;
  &nbsp;&nbsp;
  '''let''' put {front=f; back=b; size=n} x =
  '''let''' put {front=f; back=b; size=n} x =
  &nbsp;&nbsp;balance {front=f; back=x::b; size=n+1};;  
  &nbsp;&nbsp;balance {front=f; back=x::b; size=n+1};;  
''val put : 'a kolejka -> 'a -> 'a kolejka = <fun>''
  &nbsp;&nbsp;
  &nbsp;&nbsp;
  '''let''' first q =  
  '''let''' first q =  
Linia 441: Linia 736:
  &nbsp;&nbsp;&nbsp;&nbsp;{front=[]}&nbsp;&nbsp; -> failwith "Pusta kolejka" |
  &nbsp;&nbsp;&nbsp;&nbsp;{front=[]}&nbsp;&nbsp; -> failwith "Pusta kolejka" |
  &nbsp;&nbsp;&nbsp;&nbsp;{front=x::_} ->  x;;
  &nbsp;&nbsp;&nbsp;&nbsp;{front=x::_} ->  x;;
  &nbsp;&nbsp;&nbsp;&nbsp;
  ''val first : 'a kolejka -> 'a = <fun>''
&nbsp;&nbsp;
  '''let''' remove q =
  '''let''' remove q =
  &nbsp;&nbsp;'''match''' q '''with'''
  &nbsp;&nbsp;'''match''' q '''with'''
  &nbsp;&nbsp;&nbsp;&nbsp;{front=_::f} -> balance {front=f; back=q.back; size=q.size-1}  |
  &nbsp;&nbsp;&nbsp;&nbsp;{front=_::f} -> balance {front=f; back=q.back; size=q.size-1}  |
  &nbsp;&nbsp;&nbsp;&nbsp;_&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-> empty_queue;;
  &nbsp;&nbsp;&nbsp;&nbsp;_&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-> empty_queue;;
       
''val remove : 'a kolejka -> 'a kolejka = <fun>''
Zanalizujmy koszt zamortyzowany operacji (zakładając, że pamiętamy jedną kolejkę na raz). Funkcja amortyzacji to długość drugiej listy składowej. Procedury <tt>empty</tt>, <tt>size</tt> i <tt>first</tt> mają koszt stały i nie zmieniają funkcji amortyzacji.
 
Dla pustej kolejki funkcja amortyzacji jest równa 0. Procedura <tt>put</tt> ma koszt stały i powoduje zwiększeniefunkcji amortyzacji o 1, co daje stały koszt zamortyzowany. Procedura <tt>remove</tt> może mieć koszt liniowy, ze względu naodwracanie elementów na liście. Jednak ta ostatnia wykonuje tyle kroków, o ile zmniejsza funkcję amortyzacji. Tak więc koszt zamortyzowany procedury <tt>remove</tt> jest stały.
<p align="justify">
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>


Drzewa binarne możemy zadeklarować tak:
       
  '''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 jednak nie był parametr <tt>`a</tt>, to wszystkie węzły drzewa muszą zawierać wartości tego samego typu.  
 
       
<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;;
  &nbsp;&nbsp;
  ''exception Undefined''
  (* Pusty zbiór to puste drzewo *)
  (* Pusty zbiór to puste drzewo *)
  '''let''' empty_finset = Empty;;
  '''let''' empty_finset = Empty;;
  &nbsp;&nbsp;
  ''val empty_finset : 'a finset = Empty''
  (* Znajdź poddrzewo o zadanym kluczu&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*)
  (* Znajdź poddrzewo o zadanym kluczu&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*)
  (* (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 =
&nbsp;&nbsp;'''match''' s '''with'''
  '''match''' s '''with'''
&nbsp;&nbsp;&nbsp;&nbsp;Empty -> Empty |
    Empty -> Empty |
&nbsp;&nbsp;&nbsp;&nbsp;Node (k, l, r) ->
    Node (k, l, r) ->
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''if''' e = k '''then''' s
      '''if''' e = k '''then''' s
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''else''' '''if''' e < k '''then''' find l e
      '''else''' '''if''' e < k '''then''' find l e
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''else''' find r e;;
      '''else''' find r e;;
  &nbsp;&nbsp;
  ''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);;
  &nbsp;&nbsp;
  ''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 =
&nbsp;&nbsp;'''match''' s '''with'''
  '''match''' s '''with'''
&nbsp;&nbsp;&nbsp;&nbsp;Empty -> Node (e, Empty, Empty) |
    Empty -> Node (e, Empty, Empty) |
&nbsp;&nbsp;&nbsp;&nbsp;Node (k, l, r) ->
    Node (k, l, r) ->
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''if''' e = k '''then'''
      '''if''' e = k '''then'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;s
        s
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''else''' if e < k '''then'''
      '''else''' if e < k '''then'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Node (k, insert l e, r)
        Node (k, insert l e, r)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''else'''
      '''else'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Node (k, l, insert r e);;
        Node (k, l, insert r e);;
  &nbsp;&nbsp;
  ''val insert : 'a finset -> 'a -> 'a finset = <fun>''
  (* Najmniejszy element *)
  (* Najmniejszy element *)
  '''let''' '''rec''' min s =
  '''let''' '''rec''' min s =
&nbsp;&nbsp;'''match''' s '''with'''
  '''match''' s '''with'''
&nbsp;&nbsp;&nbsp;&nbsp;Node (e, Empty, _) -> e |
    Node (e, Empty, _) -> e |
&nbsp;&nbsp;&nbsp;&nbsp;Node (_, l, _)&nbsp;&nbsp;&nbsp;&nbsp; -> min l |
    Node (_, l, _)     -> min l |
&nbsp;&nbsp;&nbsp;&nbsp;Empty&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-> '''raise'''     Undefined;;  
    Empty             -> '''raise''' Undefined;;  
  &nbsp;&nbsp;&nbsp;&nbsp;
  ''val min : 'a finset -> 'a = <fun>''
  (* Największy&nbsp;&nbsp;element *)
  (* Największy&nbsp;&nbsp;element *)
  '''let''' '''rec''' max s =
  '''let''' '''rec''' max s =
&nbsp;&nbsp;'''match''' s '''with'''
  '''match''' s '''with'''
&nbsp;&nbsp;&nbsp;&nbsp;Node (e, _, Empty) -> e |
    Node (e, _, Empty) -> e |
&nbsp;&nbsp;&nbsp;&nbsp;Node (_, _, r)&nbsp;&nbsp;&nbsp;&nbsp; -> max r |
    Node (_, _, r)     -> max r |
&nbsp;&nbsp;&nbsp;&nbsp;Empty&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-> '''raise''' Undefined;;
    Empty             -> '''raise''' Undefined;;
  &nbsp;&nbsp;&nbsp;&nbsp;
  ''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 =  
&nbsp;&nbsp;'''match''' s '''with'''
  '''match''' s '''with'''
&nbsp;&nbsp;&nbsp;&nbsp;Empty -> s |
    Empty -> s |
&nbsp;&nbsp;&nbsp;&nbsp;Node (k, l, r) ->
    Node (k, l, r) ->
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''if''' e = k '''then'''
      '''if''' e = k '''then'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''match''' (l, r) '''with'''
        '''match''' (l, r) '''with'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;(Empty, Empty) -> Empty |
          (Empty, Empty) -> Empty |
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;(Empty, _) ->
          (Empty, _) ->
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''let''' m = min r
            '''let''' m = min r
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''in''' Node (m, l, remove r m) |
            '''in''' Node (m, l, remove r m) |
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_&nbsp;&nbsp;->
          _ ->
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''let''' m = max l
            '''let''' m = max l
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''in''' Node (m, remove l m, r)
            '''in''' Node (m, remove l m, r)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''else''' '''if''' e < k '''then'''
      '''else''' '''if''' e < k '''then'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Node (k, remove l e, r)
        Node (k, remove l e, r)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''else'''
      '''else'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Node (k, l, remove r e);;
        Node (k, l, remove r e);;
''val remove : 'a finset -> 'a -> 'a finset = <fun>''


==Laboratoria i ćwiczenia==
== Ćwiczenia ==


[[Programowanie funkcyjne/Struktury danych/Ćwiczenia        |Laboratoria i ćwiczenia do wykładu]]
[[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]

Konstrukcja match-with z poprzedniego przykładu jest rozwijana w następujący sposób:
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 n-tek wartości prostszych typów. Produkt kartezjański typów t1,,tn jest oznaczany przez t1**tn. Każdemu typowi produktowemu towarzyszy jeden konstruktor postaci: (x1,,xn). Wartością takiego konstruktora jest n-tka podanych wartości. Na wartościach produktów kartezjańskich jest określona równość. Dwie n-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 n-tki wartości typu t to po prostu wartości typu t, (x)=x. 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ę n elementową nie musimy pisać: x1::(x2::::(xn::[])), lecz możemy użyć skrótu notacyjnego: [x1;x2;;xn]. 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 n-tce, ale współrzędne (tzw. pola rekordu) są identyfikowane raczej po nazwie, a nie wg. pozycji w n-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
Uwaga

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

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

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

[x1;;xk]

, lista tylna

[y1;;yl]

reprezentują kolejkę zawierającą

k+l

elementów:

x1,,xk,yl,,y1

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

Ćwiczenia

Ćwiczenia