Programowanie funkcyjne/Moduły: Różnice pomiędzy wersjami
Linia 217: | Linia 217: | ||
Powyższe zasady są znane pod nazwą ''separation of concerns''. Rozważając kilka możliwych podziałów na moduły możemy porównać je stosując następujący eksperyment myślowy. Przygotowujemy listę potencjalnych zmian w programie. Lista ta nie może być wydumana, ani nie może to być lista zmian, które łatwo wprowadzić do programu, ale lista realnych zmian, które mogą wynikać z potrzeb użytkownika programu. Dla każdej z tych zmian i każdego z podziałów na moduły badamy ile modułów należy zmodyfikować w celu prowadzenia danej zmiany. Im więcej modułów, tym gorzej. | Powyższe zasady są znane pod nazwą ''separation of concerns''. Rozważając kilka możliwych podziałów na moduły możemy porównać je stosując następujący eksperyment myślowy. Przygotowujemy listę potencjalnych zmian w programie. Lista ta nie może być wydumana, ani nie może to być lista zmian, które łatwo wprowadzić do programu, ale lista realnych zmian, które mogą wynikać z potrzeb użytkownika programu. Dla każdej z tych zmian i każdego z podziałów na moduły badamy ile modułów należy zmodyfikować w celu prowadzenia danej zmiany. Im więcej modułów, tym gorzej. | ||
<br/> | |||
Przykład: | Przykład: [D.L.Parnas,''On the Criteria To Be Used in Decomposing Systems into Modules'', CACM 12 (15), 1972] | ||
[D.L.Parnas, | |||
''On the Criteria To Be Used in Decomposing Systems into Modules'', CACM 12 (15), 1972] | |||
Przykład problemu: tekst, rotacje cykliczne wierszy. Możliwe podziały mogą organizować się albo wokół faz działania | Przykład problemu: tekst, rotacje cykliczne wierszy. Możliwe podziały mogą organizować się albo wokół faz działania | ||
programu, albo wokół danych. Ten drugi sposób jest lepszy. | programu, albo wokół danych. Ten drugi sposób jest lepszy. | ||
</p> | </p> |
Wersja z 19:05, 19 lip 2006
Wprowadzenie
Każdy duży system powinien być podzielony na mniejsze, łatwiejsze do ogarnięcia składowe. Jednocześnie, interakcje między tymi składowymi powinny być ograniczone do niezbędnego minimum. Stosując taką strukturalizację jesteśmy w stanie ogarnąć, jeśli nie
cały system, to jedną składową na raz, wraz ze wszystkimi elementami mającymi wpływ na nią. W przypadku programów, takie składowe to moduły.
Moduł można określić jako fragment systemu polegający na wykonaniu wyodrębnionego zadania programistycznego. Każdy moduł ma ściśle określony interfejs --- zestaw obiektów programistycznych, które realizuje. Jedne moduły mogą korzystać z obiektów zaimplementowanych przez inne. Zwykle sformułowaniu zadania programistycznego towarzyszy (mniej lub bardziej formalna) specyfikacja własności, które powinny posiadać obiekty programistyczne implementowane przez moduł.
Moduł ma charakter czarnej skrzynki (ang. black-box approach). Na zewnątrz modułu widoczne są wyłącznie te obiekty programistyczne, które tworzą interfejs. Natomiast sposób ich implementacji, jak i ew. obiekty pomocnicze
są ukryte wewnątrz modułu. Specyfikacja modułu nie powinna odwoływać się do sposobu implementacji modułu, a jedynie do takich właściwości modułu, które może zaobserwować użytkownik modułu. Co więcej, użytkownik nie tylko nie ma możliwości, ale i nie
powinien (w swym programie) wnikać w sposób implementacji modułu (ang. information hiding). Takie zasady konstrukcji modułów pozwalają (po podzieleniu systemu na moduły) na niezależne ich opracowywanie. Podobnie, moduły można też niezależnie kompilować.
Na poziomie języka programowania, wszystkie informacje konieczne do skompilowania modułu są zawarte w interfejsach wykorzystywanych przez niego modułów.
W Ocamlu możemy osobno definiować interfejsy i implementacje modułów. Interfejsy modułów nazywamy sygnaturami natomiast ich implementacje strukturami.Dodatkowo istnieje pojęcie modułów sparametryzowanych --- funktorów.Są to moduły, które mają określony interfejs, a także korzystają z modułów o określonych interfejsach, ale nie sprecyzowanej implementacji. Dopiero po podaniu implementacji uzyskujemy wynikowy moduł. Funktory można traktować jak konstrukcje przekształcające moduły w moduły.
Proste struktury
Definiując strukturę zbieramy razem definicje pojęć, które ta struktura ma udostępniać, otaczamy je słowami struct ... end i nadajemy modułowi nazwę.
<definicja> ::= module <Identyfikator> = <struktura> <struktura> ::= struct { <definicja> }* end
Tak zdefiniowana struktura udostępnia wszystkie pojęcia zdefiniowane wewnątrz --- ma największy z możliwych interfejsów. Do pojęć zdefiniowanych wewnątrz struktury możemy dostać się stosując nazwy kwalifikowane postaci:
<identyfikator> ::= <Identyfikator> . <identyfikator>
Możemy też "otworzyć" strukturę, tzn. wyłuskać z niej wszystkieudostępniane przez nią pojęcia tak, aby były dostępne bez kwalifikowania nazwą modułu.
<jednostka kompilacji> ::= open <Identyfikator>
Przykład [Proste przykłady modułów]
module Modulik = struct type typik = int list let lista = [2; 3; 7] let prod l = fold ( * ) 1 l end;; Modulik.prod Modulik.lista;; open Modulik;; prod lista;; module Pusty = struct end;;
Przykład [Moduły mogą zwierać wewnątrz moduły]
module M = struct module A = struct let a = 27 end module B = struct let b = 15 end end;; M.A.a + M.B.b;;
Sygnatury
Sygnatura, to interfejs modułu --- określa, które elementy struktury mają być dostępne na zewnątrz. Wszystko czego nie widać w sygnaturze jest ukryte. Sygnatura może zawierać deklaracje:
- wartości, z podaniem typu wartości,
- typu wraz z jego definicją,
- typu abstrakcyjnego (bez podania definicji),
- sygnatury lokalnej
- wyjątków.
<definicja> ::= module type <Identyfikator> = <sygnatura> <sygnatura> ::= sig { <deklaracja> }* end <deklaracja> ::= type { <parametr typowy> }* <identyfikator> [ = <typ> ] | val <identyfikator> : <typ> | module type <Identyfikator> = <sygnatura> | exception <wariant>
Przykład [Taka sobie sygnatura]
module type S = sig type abstrakcyjny type konkretny = int * float val x : abstrakcyjny * konkretny module type Pusty = sig end exception Wyjatek of abstrakcyjny end;;
Przykład [Sygnatura kolejek FIFO]
module type FIFO = sig exception EmptyQueue type 'a queue val empty : 'a queue val insert : 'a queue -> 'a -> 'a queue val front : 'a queue -> 'a val remove : 'a queue -> 'a queue end;;
Sygnatury można rozszerzać. Definiując jedną sygnaturę można "wciągnąć" do niejzawartość innej sygnatury.
Przykład [Sygnatura kolejek dwustronnych]
module type QUEUE = sig include FIFO val back : 'a queue -> 'a val insert_front : 'a queue -> 'a -> 'a queue val remove_back : 'a queue -> 'a queue end;;
Sygnatury można ukonkretniać. Na przykład, jeżeli sygnatura zawiera typ abstrakcyjny, to można go ukonkretnić. Można podać jaka ma być jego implementacja.
Przykład [Ukonkretnianie sygnatur]
module type LIST = FIFO with type 'a queue = 'a list;;
Łączenie struktur i sygnatur
Podając jawnie sygnaturę dla struktury ograniczamy wgląd do środka sygnatury. Można to zrobić na kilka sposobów: treść sygnatury i struktury można podać explicite, lub odwołać się do zdefiniowanej sygnatury/struktury.
<definicja> ::= module <Identyfikator> [ : <sygnatura> ] = <struktura> <struktura> ::= <struktura> : <sygnatura> <struktura> ::= <Identyfikator> <sygnatura> ::= <Identyfikator>
Przykład [Różne sposoby definiowania modułu FIFO]
module Fifo_implementation = struct exception EmptyQueue type 'a queue = 'a list let empty = [] let insert q x = q @ [x] let front q = match q with [] -> raise EmptyQueue | h::_ -> h let remove q = match q with [] -> raise EmptyQueue | _::t -> t end;; module Fifo : FIFO = Fifo_implementation;; module Fifo = (Fifo_implementation : FIFO);; module Fifo : sig ... end = struct ... end;;
Podobnie jak można rozszerzać sygnatury, można też rozszerzać moduły:
Przykład [Liczby wymierne z dwiema barierami abstrakcji]
module type ULAMKI = sig type t val ulamek : int -> int -> t val licznik : t -> int val mianownik : t -> int end;; module type RAT = sig include ULAMKI val plus : t -> t -> t val minus : t -> t -> t val razy : t -> t -> t val podziel : t -> t -> t val rowne : t -> t -> bool end;; module Ulamki : ULAMKI = struct type t = int * int let ulamek l m = (l, m) let licznik (l, _) = l let mianownik (_, m) = m end;; module Rat : RAT = struct include Ulamki let plus x y = ulamek (licznik x * mianownik y + licznik y * mianownik x) (mianownik x * mianownik y) let minus x y = ulamek (licznik x * mianownik y - licznik y * mianownik x) (mianownik x * mianownik y) let razy x y = ulamek (licznik x * licznik y) (mianownik x * mianownik y) let podziel x y = ulamek (licznik x * mianownik y) (mianownik x * licznik y) let rowne x y = (licznik x * mianownik y) = (licznik y * mianownik x) end;;
Jak wyodrębniać moduły?
Jakimi zasadami kierować się dzieląc program na moduły? Każdy program można podzielić na moduły: pierwsze 100 linii, drugie 100 linii, itd. Oczywiście nie każdy podział jest właściwy. Podział programu na moduły powinien być taki, aby:
- powiązania między modułami były jak najmniejsze;
- jak najmniej szczegółów budowy jednego modułu miało wpływ na budowę innego modułu,
- jeden moduł powinien koncentrować się na jednej decyzji
- projektowej, jednym "sekrecie";
- nie należy łączyć nie związanych ze sobą sekretów w jednym module.
Powyższe zasady są znane pod nazwą separation of concerns. Rozważając kilka możliwych podziałów na moduły możemy porównać je stosując następujący eksperyment myślowy. Przygotowujemy listę potencjalnych zmian w programie. Lista ta nie może być wydumana, ani nie może to być lista zmian, które łatwo wprowadzić do programu, ale lista realnych zmian, które mogą wynikać z potrzeb użytkownika programu. Dla każdej z tych zmian i każdego z podziałów na moduły badamy ile modułów należy zmodyfikować w celu prowadzenia danej zmiany. Im więcej modułów, tym gorzej.
Przykład: [D.L.Parnas,On the Criteria To Be Used in Decomposing Systems into Modules, CACM 12 (15), 1972]
Przykład problemu: tekst, rotacje cykliczne wierszy. Możliwe podziały mogą organizować się albo wokół faz działania
programu, albo wokół danych. Ten drugi sposób jest lepszy.