Programowanie funkcyjne/Funktory: Różnice pomiędzy wersjami
Nie podano opisu zmian |
Nie podano opisu zmian |
||
Linia 1: | Linia 1: | ||
==Wstęp== | ==Wstęp== | ||
<p align="justify"> | |||
Zwykle jedne moduły korzystają z innych modułów. | |||
Zgodnie z zasadą ukrywania informacji (ang. ''information hiding'') istotne powinny być jedynie specyfikacje wykorzystywanych modułów, a nie ich konkretne implementacje. | |||
Podstawienie dowolnej implementacji o zadanej sygnaturze i spełniającej zadaną specyfikację powinno być | |||
akceptowalne z punktu widzenia modułów korzystających z takiej implementacji. | |||
</p> | |||
<p align="justify"> | |||
Kolejka priorytetowa. Na elementach kolejki musi być określony porządek liniowy. | Skoro rozdzieliliśmy pojęcia interfejsu (sygnatury) i implementacji (struktury), to możemy zapisywać moduły korzystające z innych modułów w bardziej abstrakcyjny sposób. | ||
Zamiast określać dokładnie z jakiej implementacji korzysta dany moduł, określamy jedynie jej sygnaturę | |||
(i przez domniemanie również specyfikację). | |||
Otrzymujemy w ten sposób '''funktory'''. | |||
</p> | |||
<p align="justify"> | |||
Funktory to moduły sparametryzowane. | |||
Ich parametrami są struktury, o określonych sygnaturach, z których mają one korzystać. | |||
Dopiero gdy podstawimy w miejsce parametrów konkretne struktury, | |||
otrzymujemy strukturę implementowaną przez funktor. | |||
</p> | |||
<p align="justify"> | |||
Zwykłe moduły (struktury) tworzą statyczną sieć powiązań. | |||
Natomiast moduły są jak wymienne elementy, klocki, które możemy łączyć na rozmaite sposoby. | |||
</p> | |||
<p align="justify"> | |||
Na funktory możemy też patrzeć jak na funkcje na strukturach --- przetwarzają one struktury-parametry w struktury wynikowe. | |||
Należy jednak pamiętać, że podstawianie parametrów funktorów dokonuje się w sposób statyczny -- w czasie kompilacji. | |||
</p> | |||
== Proste funktory == | |||
<p align="justify"> | |||
Tak samo jak w przypadku struktur, w przyapdku funktorów mamy pojęcie sygnatury. | |||
Sygnatura funktora określa sygnaturę parametrów oraz sygnaturę wyniku. | |||
</p> | |||
<sygnatura> ::= <u>functor</u> <u>(</u> <Identyfikator> <u>:</u> <sygnatura> <u>)</u> <u>-></u> <sygnatura> | |||
<struktura> ::= <u>functor</u> <u>(</u> <Identyfikator> <u>:</u> <sygnatura> <u>)</u> <u>-></u> <struktura> | | |||
<struktura> <u>(</u> <struktura <u>)</u> | |||
<p align="justify"> | |||
Jeżeli funktor ma więcej niż jeden parametr, to tak jak w przypadku procedur, zapisujemy go jako | |||
funktor z pierwszym parametrem, którego wynikiem jest funktor z drugim parametrem itd. | |||
Z powyższej składni widać też, że funktory mogą być nie tylko wynikiem, ale i parametrami funktorów, | |||
czyli mamy funktory wyższych rzędów. | |||
Zajmiemy się nimi dalej. | |||
</p> | |||
{{przyklad|[Kolejka priorytetowa]||}} | |||
<p align="justify"> | |||
Przypuśćmy, że chcemy zaimplementować kolejkę priorytetową elementów bliżej nieokreślonego typu. | |||
Na elementach kolejki musi być określony porządek liniowy, zgodnie z którym będziemy szeregować elementy w kolejce. | |||
Kolejkę zaimplementujemy w postaci funktora. | |||
Parametrem funktora będzie struktura określająca typ elementów i porządek liniowy na nich. | |||
</p> | |||
'''type''' porownanie = Mniejsze | Rowne | Wieksze;; | '''type''' porownanie = Mniejsze | Rowne | Wieksze;; | ||
'''module''' '''type''' PORZADEK_LINIOWY = | '''module''' '''type''' PORZADEK_LINIOWY = | ||
'''sig''' | |||
'''type''' t (* Typ elementów. *) | |||
'''val''' porownaj : t -> t -> porownanie (* Porządek liniowy. *) | |||
'''end''';; | |||
Kolejka priorytetowa powinna mieć sygnaturę postaci: | Kolejka priorytetowa powinna mieć sygnaturę postaci: | ||
'''module''' '''type''' PRI_QUEUE_FUNC = '''functor''' (Elem : PORZADEK_LINIOWY) -> | '''module''' '''type''' PRI_QUEUE_FUNC = '''functor''' (Elem : PORZADEK_LINIOWY) -> | ||
'''sig''' | |||
'''exception''' EmptyQueue | |||
'''type''' elem = Elem.t (* Typ elementów. *) | |||
'''type''' queue (* Typ kolejki. *) | |||
'''val''' empty : queue (* Pusta kolejka. *) | |||
'''val''' put : elem -> queue -> queue (* Wstawienie do kolejki. *) | |||
'''val''' min : queue -> elem (* Element najmniejszy. *) | |||
'''val''' removemin : queue -> queue (* Usunięcie elementu najmniejszego. *) | |||
'''end''';; | |||
Zwróćmy uwagę na to, że typ <tt>queue</tt> jest abstrakcyjny, | <p align="justify"> | ||
Zwróćmy uwagę na to, że typ <tt>queue</tt> jest abstrakcyjny. | |||
Ukrywamy w ten sposób strukturę kolejki. | |||
Zwróćmy też uwagę na to, że typ <tt>elem</tt> jest określony jako <tt>Elem.t</tt>. | |||
Jest to specyficzna konstrukcja. | |||
Przy jej zastosowaniu, cokolwiek będzie wiadome o typie <tt>Elem.t</tt> będzie również wiadome o typie <tt>elem</tt>, | |||
mimo, że w sygnaturze <tt>PORZADEK_LINIOWY</tt> typ <tt>t</tt> jest typem abstrakcyjnym. | |||
Implementację kolejki priorytetowej możemy zrealizować w następujący sposób: | |||
</p> | |||
'''module''' PriQueue : PRI_QUEUE_FUNC = | '''module''' PriQueue : PRI_QUEUE_FUNC = | ||
'''functor''' (Elem : PORZADEK_LINIOWY) -> | |||
'''struct''' | |||
'''type''' elem = Elem.t | |||
'''exception''' EmptyQueue | |||
'''type''' queue = elem list | |||
'''let''' empty = [] | |||
'''let''' '''rec''' put x q = | |||
'''match''' q '''with''' | |||
[] -> [x] | | |||
h::t -> '''if''' Elem.porownaj x h = Mniejsze '''then''' | |||
x :: q | |||
'''else''' | |||
h::(put x t) | |||
'''let''' min q = | |||
'''match''' q '''with''' | |||
[] -> '''raise''' EmptyQueue | | |||
h::_ -> h | |||
'''let''' removemin (q:queue) = | |||
'''match''' q '''with''' | |||
[] -> '''raise''' EmptyQueue | | |||
_::t -> t | |||
'''end''';; | |||
Kolejka priorytetowa liczb całkowitych może być zrealizowana | <p align="justify"> | ||
Kolejka priorytetowa liczb całkowitych może być zrealizowana poprzez podstawienie jako argumentu funktora następującej struktury: | |||
'''module''' IntOrder | </p> | ||
'''module''' IntOrder = | |||
'''struct''' | |||
'''type''' t = int | |||
'''let''' porownaj (x:int) (y:int) = | |||
'''if''' x < y '''then''' Mniejsze '''else''' | |||
'''if''' x > y '''then''' Wieksze '''else''' Rowne | |||
'''end''';; | |||
'''module''' IntQueue = PriQueue (IntOrder);; | '''module''' IntQueue = PriQueue (IntOrder);; | ||
IntQueue.min (IntQueue.put 42 IntQueue.empty);; | |||
''- : IntQueue.elem = 42'' | |||
<p align="justify"> | |||
Zwróćmy uwagę, że nie ograniczamy struktury <tt>IntOrder</tt> do sygnatury <tt>PORZADEK_LINIOWY</tt>. | |||
Chcemy, aby typ <tt>t</tt> nie był typem abstrakcyjnym, lecz był równoważny <tt>int</tt>. | |||
Dzięki temu będzie wiadomo, że elementy kolejki są również typu <tt>int</tt>. | |||
</p> | |||
<p align="justify"> | |||
Dla porównania spójrzmy na poniższy przykład. | |||
Jest to definicja modułu, którego nie da się używać, gdyż typ elementów jest abstrakcyjny i nie jesteśmy w stanie utwożyć jakiegokolwiek elementu. | |||
</p> | |||
'''module''' SomeOrder : PORZADEK_LINIOWY = | |||
'''struct''' | |||
'''type''' t = int | |||
'''let''' porownaj (x:int) (y:int) = | |||
'''if''' x < y '''then''' Mniejsze '''else''' | |||
'''if''' x > y '''then''' Wieksze '''else''' Rowne | |||
'''end''';; | |||
'''module''' SomeQueue = PriQueue (SomeOrder);; | |||
SomeQueue.min (SomeQueue.put <u>42</u> SomeQueue.empty);; | |||
''This expression has type int but is here used with type SomeQueue.elem = SomeOrder.t'' | |||
{{przyklad|[Sortowanie]||}} | |||
<p align="justify"> | |||
Innym podstawowym pojęciem związanym z porządkami liniowymi jest sortowanie. | |||
Również możemy je zapisać w postaci funktora sparametryzowanego porządkiem liniowym. | |||
</p> | |||
'''module''' '''type''' SORTING = '''functor''' (Elem : PORZADEK_LINIOWY) -> | '''module''' '''type''' SORTING = '''functor''' (Elem : PORZADEK_LINIOWY) -> | ||
'''sig''' | |||
'''type''' elem = Elem.t (* Typ elementów. *) | |||
'''val''' sortuj : elem list -> elem list (* Procedura sortująca listy elementów. *) | |||
'''end''';; | |||
Jeden z algorytmów sortowania, to heap-sort. | <p align="justify"> | ||
Jeden z możliwych algorytmów sortowania, to heap-sort. | |||
Możemy go zapisać w postaci funktora sparametryzowanego implementacją kolejek priorytetowych. | |||
</p> | |||
'''module''' HeapSort (PrQ : PRI_QUEUE_FUNC): SORTING = | '''module''' HeapSort (PrQ : PRI_QUEUE_FUNC): SORTING = | ||
'''functor''' (Elem : PORZADEK_LINIOWY) -> | |||
'''struct''' | |||
'''type''' elem = Elem.t | |||
'''module''' PQ = PrQ (Elem) | |||
'''open''' PQ | |||
'''let rec''' wkladaj l = | |||
List.fold_left (fun h x -> put x h) empty l | |||
'''let''' '''rec''' wyjmuj q a = | |||
'''if''' q = empty '''then''' | |||
List.rev a | |||
'''else''' | |||
wyjmuj (removemin q) (min q :: a) | |||
'''let''' sortuj l = wyjmuj (wkladaj l) [] | |||
'''end''';; | |||
'''module''' Sortowanie = HeapSort (PriQueue);; | '''module''' Sortowanie = HeapSort (PriQueue);; | ||
'''module''' S = Sortowanie (IntOrder);; | '''module''' S = Sortowanie (IntOrder);; | ||
S.sortuj [1;7;3;6;5;2;4;8];; | S.sortuj [1;7;3;6;5;2;4;8];; | ||
''- : S.elem list = [1; 2; 3; 4; 5; 6; 7; 8]'' | |||
Możliwy jest również taki zapis: | |||
'''module''' Sort = HeapSort (PriQueue) (IntOrder);; | |||
Sort.sortuj [1;7;3;6;5;2;4;8];; | |||
''- : Sort.elem list = [1; 2; 3; 4; 5; 6; 7; 8]'' | |||
==Funktory wyższych rzędów== | ==Funktory wyższych rzędów== | ||
<p align="justify"> | |||
Tak jak istnieją procedury wyższych rzędów, istnieją również funktory wyższych rzędów. | |||
Są to funktory, których parametrami i/lub wynikiem są funktory. | |||
Oto przykład, stanowiący kontynuację przykładu z poprzedniego punktu: | |||
</p> | |||
{{przyklad|[Mediana]||}} | |||
<p align="justify"> | |||
Jeden ze sposobów wyznaczania mediany ciągu (nie najbardziej efektywny, ale za to prosty) polega na posortowaniu ciągu i wybraniu elementu środkowego. | |||
Taki algorytm możemy zapisać w postaci funktora sparametryzowanego algorytmem sortowania i porządkiem liniowym. (Zauważmy, że sortowanie jest tu również funktorem.) | |||
</p> | |||
'''module | '''module type''' MEDIANA = | ||
functor (Elem : PORZADEK_LINIOWY) -> | |||
'''sig''' | |||
'''type''' elem = Elem.t | |||
'''exception''' PustaLista | |||
'''val''' mediana : elem list -> elem | |||
'''end''';; | |||
'''module''' Mediana : '''functor''' (Sort : SORTING) -> MEDIANA = | '''module''' Mediana : '''functor''' (Sort : SORTING) -> MEDIANA = | ||
'''functor''' (Sort : SORTING) -> | |||
'''functor''' (Elem : PORZADEK_LINIOWY) -> | |||
'''struct''' | |||
'''type''' elem = Elem.t | |||
'''exception''' PustaLista | |||
'''module''' S = Sort (Elem) | |||
'''let''' mediana l = | |||
'''let''' s = S.sortuj l | |||
'''and''' n = List.length l | |||
'''in''' | |||
'''if''' n = 0 '''then''' | |||
'''raise''' PustaLista | |||
'''else''' | |||
List.nth s (n / 2) | |||
'''end''';; | |||
'''module''' M = Mediana (HeapSort (PriQueue)) (IntOrder);; | '''module''' M = Mediana (HeapSort (PriQueue)) (IntOrder);; | ||
M.mediana [4;3;5;6;2;1;7];; | M.mediana [4;3;5;6;2;1;7];; | ||
<p align="justify"> | |||
Jak widać z powyższego przykładu, jeżeli będziemy zapisywać moduły w postaci funktorów, | |||
to będziemy z nich tworzyć łatwo wymienne elementy. | |||
Tworząc funktory wyższych rzędów zwiększamy naszą elastyczność i możemy łączyć rozmaite algorytmy i struktury danych w praktycznie dowolne kombinacje. | |||
</p> | |||
== | == Ćwiczenia == | ||
[[Programowanie funkcyjne/Funktory/Ćwiczenia | | [[Programowanie funkcyjne/Funktory/Ćwiczenia | Ćwiczenia]] |
Wersja z 23:41, 22 wrz 2006
Wstęp
Zwykle jedne moduły korzystają z innych modułów. Zgodnie z zasadą ukrywania informacji (ang. information hiding) istotne powinny być jedynie specyfikacje wykorzystywanych modułów, a nie ich konkretne implementacje. Podstawienie dowolnej implementacji o zadanej sygnaturze i spełniającej zadaną specyfikację powinno być akceptowalne z punktu widzenia modułów korzystających z takiej implementacji.
Skoro rozdzieliliśmy pojęcia interfejsu (sygnatury) i implementacji (struktury), to możemy zapisywać moduły korzystające z innych modułów w bardziej abstrakcyjny sposób. Zamiast określać dokładnie z jakiej implementacji korzysta dany moduł, określamy jedynie jej sygnaturę (i przez domniemanie również specyfikację). Otrzymujemy w ten sposób funktory.
Funktory to moduły sparametryzowane. Ich parametrami są struktury, o określonych sygnaturach, z których mają one korzystać. Dopiero gdy podstawimy w miejsce parametrów konkretne struktury, otrzymujemy strukturę implementowaną przez funktor.
Zwykłe moduły (struktury) tworzą statyczną sieć powiązań. Natomiast moduły są jak wymienne elementy, klocki, które możemy łączyć na rozmaite sposoby.
Na funktory możemy też patrzeć jak na funkcje na strukturach --- przetwarzają one struktury-parametry w struktury wynikowe. Należy jednak pamiętać, że podstawianie parametrów funktorów dokonuje się w sposób statyczny -- w czasie kompilacji.
Proste funktory
Tak samo jak w przypadku struktur, w przyapdku funktorów mamy pojęcie sygnatury. Sygnatura funktora określa sygnaturę parametrów oraz sygnaturę wyniku.
<sygnatura> ::= functor ( <Identyfikator> : <sygnatura> ) -> <sygnatura> <struktura> ::= functor ( <Identyfikator> : <sygnatura> ) -> <struktura> | <struktura> ( <struktura )
Jeżeli funktor ma więcej niż jeden parametr, to tak jak w przypadku procedur, zapisujemy go jako funktor z pierwszym parametrem, którego wynikiem jest funktor z drugim parametrem itd. Z powyższej składni widać też, że funktory mogą być nie tylko wynikiem, ale i parametrami funktorów, czyli mamy funktory wyższych rzędów. Zajmiemy się nimi dalej.
Przykład [Kolejka priorytetowa]
Przypuśćmy, że chcemy zaimplementować kolejkę priorytetową elementów bliżej nieokreślonego typu. Na elementach kolejki musi być określony porządek liniowy, zgodnie z którym będziemy szeregować elementy w kolejce. Kolejkę zaimplementujemy w postaci funktora. Parametrem funktora będzie struktura określająca typ elementów i porządek liniowy na nich.
type porownanie = Mniejsze | Rowne | Wieksze;; module type PORZADEK_LINIOWY = sig type t (* Typ elementów. *) val porownaj : t -> t -> porownanie (* Porządek liniowy. *) end;;
Kolejka priorytetowa powinna mieć sygnaturę postaci:
module type PRI_QUEUE_FUNC = functor (Elem : PORZADEK_LINIOWY) -> sig exception EmptyQueue type elem = Elem.t (* Typ elementów. *) type queue (* Typ kolejki. *) val empty : queue (* Pusta kolejka. *) val put : elem -> queue -> queue (* Wstawienie do kolejki. *) val min : queue -> elem (* Element najmniejszy. *) val removemin : queue -> queue (* Usunięcie elementu najmniejszego. *) end;;
Zwróćmy uwagę na to, że typ queue jest abstrakcyjny. Ukrywamy w ten sposób strukturę kolejki. Zwróćmy też uwagę na to, że typ elem jest określony jako Elem.t. Jest to specyficzna konstrukcja. Przy jej zastosowaniu, cokolwiek będzie wiadome o typie Elem.t będzie również wiadome o typie elem, mimo, że w sygnaturze PORZADEK_LINIOWY typ t jest typem abstrakcyjnym. Implementację kolejki priorytetowej możemy zrealizować w następujący sposób:
module PriQueue : PRI_QUEUE_FUNC = functor (Elem : PORZADEK_LINIOWY) -> struct type elem = Elem.t exception EmptyQueue type queue = elem list let empty = [] let rec put x q = match q with [] -> [x] | h::t -> if Elem.porownaj x h = Mniejsze then x :: q else h::(put x t) let min q = match q with [] -> raise EmptyQueue | h::_ -> h let removemin (q:queue) = match q with [] -> raise EmptyQueue | _::t -> t end;;
Kolejka priorytetowa liczb całkowitych może być zrealizowana poprzez podstawienie jako argumentu funktora następującej struktury:
module IntOrder = struct type t = int let porownaj (x:int) (y:int) = if x < y then Mniejsze else if x > y then Wieksze else Rowne end;; module IntQueue = PriQueue (IntOrder);; IntQueue.min (IntQueue.put 42 IntQueue.empty);; - : IntQueue.elem = 42
Zwróćmy uwagę, że nie ograniczamy struktury IntOrder do sygnatury PORZADEK_LINIOWY. Chcemy, aby typ t nie był typem abstrakcyjnym, lecz był równoważny int. Dzięki temu będzie wiadomo, że elementy kolejki są również typu int.
Dla porównania spójrzmy na poniższy przykład. Jest to definicja modułu, którego nie da się używać, gdyż typ elementów jest abstrakcyjny i nie jesteśmy w stanie utwożyć jakiegokolwiek elementu.
module SomeOrder : PORZADEK_LINIOWY = struct type t = int let porownaj (x:int) (y:int) = if x < y then Mniejsze else if x > y then Wieksze else Rowne end;; module SomeQueue = PriQueue (SomeOrder);; SomeQueue.min (SomeQueue.put 42 SomeQueue.empty);; This expression has type int but is here used with type SomeQueue.elem = SomeOrder.t
Przykład [Sortowanie]
Innym podstawowym pojęciem związanym z porządkami liniowymi jest sortowanie. Również możemy je zapisać w postaci funktora sparametryzowanego porządkiem liniowym.
module type SORTING = functor (Elem : PORZADEK_LINIOWY) -> sig type elem = Elem.t (* Typ elementów. *) val sortuj : elem list -> elem list (* Procedura sortująca listy elementów. *) end;;
Jeden z możliwych algorytmów sortowania, to heap-sort. Możemy go zapisać w postaci funktora sparametryzowanego implementacją kolejek priorytetowych.
module HeapSort (PrQ : PRI_QUEUE_FUNC): SORTING = functor (Elem : PORZADEK_LINIOWY) -> struct type elem = Elem.t module PQ = PrQ (Elem) open PQ let rec wkladaj l = List.fold_left (fun h x -> put x h) empty l let rec wyjmuj q a = if q = empty then List.rev a else wyjmuj (removemin q) (min q :: a) let sortuj l = wyjmuj (wkladaj l) [] end;; module Sortowanie = HeapSort (PriQueue);; module S = Sortowanie (IntOrder);; S.sortuj [1;7;3;6;5;2;4;8];; - : S.elem list = [1; 2; 3; 4; 5; 6; 7; 8]
Możliwy jest również taki zapis:
module Sort = HeapSort (PriQueue) (IntOrder);; Sort.sortuj [1;7;3;6;5;2;4;8];; - : Sort.elem list = [1; 2; 3; 4; 5; 6; 7; 8]
Funktory wyższych rzędów
Tak jak istnieją procedury wyższych rzędów, istnieją również funktory wyższych rzędów. Są to funktory, których parametrami i/lub wynikiem są funktory. Oto przykład, stanowiący kontynuację przykładu z poprzedniego punktu:
Przykład [Mediana]
Jeden ze sposobów wyznaczania mediany ciągu (nie najbardziej efektywny, ale za to prosty) polega na posortowaniu ciągu i wybraniu elementu środkowego. Taki algorytm możemy zapisać w postaci funktora sparametryzowanego algorytmem sortowania i porządkiem liniowym. (Zauważmy, że sortowanie jest tu również funktorem.)
module type MEDIANA = functor (Elem : PORZADEK_LINIOWY) -> sig type elem = Elem.t exception PustaLista val mediana : elem list -> elem end;; module Mediana : functor (Sort : SORTING) -> MEDIANA = functor (Sort : SORTING) -> functor (Elem : PORZADEK_LINIOWY) -> struct type elem = Elem.t exception PustaLista module S = Sort (Elem) let mediana l = let s = S.sortuj l and n = List.length l in if n = 0 then raise PustaLista else List.nth s (n / 2) end;; module M = Mediana (HeapSort (PriQueue)) (IntOrder);; M.mediana [4;3;5;6;2;1;7];;
Jak widać z powyższego przykładu, jeżeli będziemy zapisywać moduły w postaci funktorów, to będziemy z nich tworzyć łatwo wymienne elementy. Tworząc funktory wyższych rzędów zwiększamy naszą elastyczność i możemy łączyć rozmaite algorytmy i struktury danych w praktycznie dowolne kombinacje.