Wg. J. Bylina, B. Bylina „Przegląd języków i paradygmatów programowania”, UMCS, Lublin 2011 Paradygmaty programowania Paradygmat (gr. Paradeigma) - wzorzec lub przykład Def. słownikowa: przyjęty sposób widzenia rzeczywistości w danej dziedzinie W informatyce: zestaw typowych dla danej grupy języków mechanizmów dostępnych programistom oraz sposobów ich interpretacji prze semantykę języka Podstawowy podział paradygmatów i języków programowania Paradygmaty i odpowiadające im języki programowania można podzielić na dwie główne grupy: - imperatywne - deklaratywne. Języki imperatywne używają rozkazów opisujących czynności które komputer ma w pewnej kolejności wykonywać. Program jest więc listą rozkazów do wykonania, stąd nazwa imperatywne („rozkazowe"). W językach deklaratywnych programista podaje (deklaruje) komputerowi pewne zależności oraz cele, które program ma osiągnąć. Języki imperatywne mówią komputerowi, jak ma osiągnąć wynik (choć nie określają jaki wynik), natomiast języki deklaratywne opisują, co ma być osiągnięte (choć nie podają jak). Ten podział przedstawia rysunek W programowaniu imperatywnym program jest listą instrukcji (mniej lub bardziej elementarnych), które mają być wykonywane kolejno z możliwością wielokrotnego powtarzania pewnych czynności zapisanych raz czyli wykonywania pętli (iteracji). Zestaw instrukcji zawierający rozkazy operacji na pewnych danych wraz z rozkazami skoków bezwarunkowych i warunkowych (zaburzających zasadę bezpośredniego następstwa) jest istotą każdego z kodów maszynowych, najbardziej pierwotnego języka wykonywanego w maszynach o tzw. architekturze von Neumanna. Architektura von Neumanna zakłada że: — maszyna składa się z pamięci oraz jednostki centralnej, która wykonuje rozkazy (procesora); — rozkazy oraz dane zapisane są w tej samej pamięci w ten sam sposób; — rozkazy są kolejno z pamięci wczytywane do jednostki centralnej i wykonywane; — każdy rozkaz powoduję zmianę stanu maszyny rozumianego jako zawartość całej pamięci włącznie z rejestrami i znacznikami procesora; rozkazy mogą więc zmieniać wewnętrzne ustawienie jednostki centralnej, w tym miejsce, z którego będzie czytany następny rozkaz. W praktyce dzisiejsze komputery budowane są (i działają) w oparciu o architekturę von Neumanna. Zatem najbardziej naturalnym paradygmatem dla maszyny jest paradygmat imperatywny, zaś dla człowieka wygodniejszym sposobem komunikowania jest określenie, co ma być osiągnięte, bez wdawania się w detale wykonania czyli paradygmat deklaratywny. Maszyna von Neumanna i programowanie bezpośrednie ( w kodzie maszynowym) Programowanie proceduralne Programowanie proceduralne - jeden z rodzajów programowania imperatywnego. W programowaniu proceduralnym występują części kodu zwane podprogramami (procedury, funkcje, metody, operacje), które mogą być wielokrotnie — także rekurencyjnie — wywoływane z różnymi parametrami. Warto zauważyć, że programowanie proceduralne umożliwiło powstanie techniki programowania bottom-up od małych elementów do coraz większych i w końcu do całego programu. Ta technika pozwoliła na rozwój: — programowania zespołowego — każdy z programistów dostaje do wykonania swoją część zadania, z tych podprogramów budowane jest całe oprogramowanie — bibliotek oprogramowania —biblioteki są zbiorami podprogramów wykonujących pewne działania, które programiści mogą składać by po uzupełnieniu własnymi podprogramami stworzyć cały program Programowanie strukturalne To rozwinięcie paradygmatu proceduralnego Programowanie strukturalne korzysta z możliwości zapisania każdego algorytmu za pomocą elementarnych tzw. struktur sterujących : - sekwencja( bezpośrednie następstwo), czyli kolejne wykonywanie czynności - selekcja, czyli wybór działania na podstawie spełnienia jakiegoś warunku (instrukcja warunkowa z alternatywą) - pętla „dopóki", czyli ściśle określony fragment algorytmu powtarzany przy ściśle określonych warunkach - podprogram pozwalający wydzielony podalgorytm zapisać, nazwać i wywoływać wielokrotnie — (podprogramy mają dokładnie jeden punkt wejścia oraz dokładnie jeden punkt wyjścia) - rekurencja, czyli możliwość wywoływania podprogramu prze niego samego. Powyższa lista nie zawiera instrukcji skoku. Instrukcje skoku, a zwłaszcza instrukcja skoku bezwarunkowego goto uważane są za szkodliwe. Ograniczenie się do powyższych pięciu konstrukcji umożliwia stosowanie tzw. logiki Hoare'a do dowodzenia poprawności algorytmów strukturalnych. Większość współczesnych języków programowania rozszerza tę listę pozwalając na stosowanie różnego rodzaju „skoków strukturalnych", na przykład: — w wielu językach instrukcja return pozwala zakończyć wykonywanie podprogramu w różnych miejscach, a więc utworzyć wiele punktów wyjścia z jednego podprogramu; — w C i językach pochodzących od C instrukcje break oraz continue po zwalają odpowiednio na wyskoczenie z pętli oraz przeskoczenie jej części; — niektóre systemy operacyjne dostarczają funkcji systemowych (na przy kład longjump w Linuksie; także obsługa sygnałów, przypominająca nie co obsługę wyjątków), których wywołanie może spowodować przekazanie sterowania praktycznie dowolnemu miejscu w programie — a więc realnie skok w dowolne miejsce programu; — w końcu wiele języków dostarcza wprost instrukcję goto, choć czasem są nakładane pewne ograniczenia w jej stosowaniu. Programowanie strukturalne umożliwia także inną technikę programowania, mianowicie top-down. Jest to odwróceniem techniki bottom-up. Polega ona na dzieleniu całego zadania na mniejsze części zgodnie z przewidywaną strukturą na najwyższym poziomie, wypełnianiu tej struktury rozkazami elementarnymi, a następnie zastosowaniu rekurencyjnie takiego dzielenia dalej w głąb, do coraz to drobniejszych zadań. Programowanie obiektowe Najbardziej rozpowszechnionym w dzisiejszych czasach paradygmatem programowania jest paradygmat obiektowy. Jest to paradygmat strukturalny rozszerzony jedynie o pojęcia klas i obiektów. Obiekty są tutaj zamkniętymi kontenerami zawierającymi dane (co przypomina rekordy czy też struktury znane z takich języków jak Pascal czy C), ale oprócz danych także podprogramy na tych danych działające, zwane metodami. Program w języku obiektowym jest nadal sekwencją rozkazów. Jednakże te rozkazy — w języku czysto obiektowym — nie są wydawane maszynie, lecz są wydawane poszczególnym obiektom, także przez inne obiekty. Takie wydzielenie danych wraz z możliwymi do wykonania na nich czynnościami (w odróżnieniu od procedur, które są opakowaniem samych czynności, oraz w odróżnieniu od wspomnianych wyżej rekordów, które są opakowaniem samych danych) pozwala na wyróżnienie pewnych cech programowania obiektowego, których próżno szukać w innych paradygmatach imperatywnych: — hermetyzacja, inaczej enkapsulacja, polegająca na tym, że tylko pewne dane i metody obiektu (stanowiące jego interfejs) są widoczne „na zewnątrz", dla innych obiektów; natomiast jego implementacja jest ukryta przed — umyślnym bądź przypadkowym „uszkodzeniem" czy też złym wykorzystaniem; — dziedziczenie pozwalające tworzyć obiekty bardziej skomplikowane na bazie prostszych; co więcej, dziedziczenie klas przekłada się na zawieranie się jednej w drugiej, a to oznacza, że obiekty mogą należeć jednocześnie do wielu klas, co ma istotne znaczenie dla polimorfizmu — abstrakcja danych wynikająca bezpośrednio z hermetyzacji i dziedziczenia — można w prosty sposób definiować ogólne obiekty (czy też klasy), które są jedynie wzorcami pewnych bardziej skomplikowanych, doprecyzowanych obiektów; — polimorfizm dynamiczny (inaczej polimorfizm obiektowy], który dzięki dziedziczeniu pozwala obiektom automatycznie dobierać odpowiednie metody do swojego aktualnego typu. Programowanie funkcyjne Funkcyjny paradygmat programowania jest podparadygmatem programowania deklaratywnego. W programowaniu funkcyjnym (funkcjonalnym), tak jak w deklaratywnym, opisujemy pożądany wynik ale w postaci funkcji. Zadaniem interpretera lub kompilatora języka funkcyjnego jest obliczenie wartości funkcji, a więc pewnego wyrażenia. W programowaniu czysto funkcyjnym funkcje zawsze przyjmują tę samą wartość dla tych samych argumentów a więc nie zależą ani od stanu maszyny, ani od urządzeń wejścia/wyjścia, użytkownika, pamięci zewnętrznej itd. W związku z pojęciem programu jako złożenia pewnych funkcji, w programowaniu funkcyjnym nie występują zmienne znane z programowania imperatywnego ani pętle, zamiast których używa się rekurencji (zmienne i pętle potrzebują dostępu do stanu maszyny, którego tu nie ma). Z drugiej strony, funkcje te mogą być takimi samymi wartościami argumentów i wyników innych funkcji jak dane - liczby, napisy, listy... Możliwe jest tutaj tzw. leniwe wartościowanie wyniku funkcji, czyli obliczanie tylko fragmentów wyniku, potrzebnych dla innej funkcji. Umożliwia to obliczanie składowych funkcji niezależnie od siebie, co pozwala na automatyczne zrównoleglanie kodu i przetwarzanie potokowe. Programowanie logiczne W programowaniu logicznym (należącym do paradygmatu deklaratywnego) — nie opisuje się drogi do rozwiązania, lecz dostarczamy maszynie zbiór przesłanek oraz tezę do dowiedzenia w postaci pytania. Maszyna ma udowodnić tezę na podstawie danych przesłanek. Wszystkie inne działania maszyny są efektami ubocznymi tego dowodzenia. . Tabela . Języki programowania a główne paradygmaty języki asemblery, „stary" BASIC, „stary" Fortran „stary" Pascal, C C++, Object Pascal, Ada Smalltalk, C#, Java Lisp, Scheme, Logo, ML, OCaml Haskell Planner, Prolog Python, Ruby SQL paradygmaty imperatywny proceduralny imperatywny proceduralny strukturalny imperatywny proceduralny strukturalny obiektowy obiektowy proceduralny funkcyjny czysto funkcyjny logiczny proceduralny strukturalny obiektowy funkcyjny deklaratywny (ale ani ściśle funkcyjny, ani ściśle logiczny) Dodatkowe paradygmaty Programowanie modularne - pośrednie między programowaniem obiektowym a proceduralnym. W tym paradygmacie głównym elementem programu jest moduł (pakiet) zawarty zwykle w osobnym pliku i w wielu aspektach traktowany jako obiekt. Języki: Ada, Haskell, Python. Programowanie aspektowe - blisko związane z powyższym. Jego celem jest podział problemu na niezależne logicznie części i ograniczenie ich liczby styków oraz ścisłe kontrolowanie każdego z nich. Język: AspectJ. Programowanie komponentowe - paradygmat związany z modularyzacją programów i z programowaniem obiektowym. Komponentami są samodzielne obiekty wyposażone w ściśle wyspecyfikowany interfejs, wykonujące pewne określone usługi. Paradygmat ten związany jest z tzw. programowaniem zdarzeniowym. Języki: Eiffel, Oberon. Programowanie agentowe - abstrakcyjna forma programowania obiektowego. Elementem jest agent , czyli wyspecjalizowany i odporny na błędy samodzielny obiekt, który w pewnym środowisku np. w sieci komputerowej może pracować sam, a w potrzebie komunikować się z innymi agentami. Działający w sieci agenci często dublują swoje czynności, po to, by zapewnić maksymalną odporność na błędy i utratę wyników. Nie bez znaczenia jest też ewentualna możliwość samoreplikacji agentów. Języki: JADE (framework Javy). Programowanie zdarzeniowe (sterowane zdarzeniami) - program składa się z wielu niezależnych podprogramów, których kolejność wykonania nie jest określona z góry przez program główny, lecz które są uruchamiane w reakcji na zaistnienie pewnych zdarzeń. Występuje w systemach operacyjnych. Obsługa wyjątków w różnych językach ma charakter programowania zdarzeniowego. Programowanie kontraktowe (związane z paradygmatem obiektowym ale także jako rozszerzenie programowania strukturalnego) - takie tworzenie kodu, by mógł być on automatycznie sprawdzony (pod względem zgodności ze specyfikacją) i ewentualnie przetestowany. Języki: Eiffel, interfejsy w Javie. Programowanie generyczne (inaczej: uogólnione, rodzajowe) umożliwia tworzenie jednostek (klas, obiektów, funkcji, typów) parametrycznych, (polimorficznych, uogólnionych), które stają się pełnoprawnymi jednostkami w chwili ich dookreślenia przy skorzystaniu z ich definicji w gotowym programie. Języki: Ada, C++, Haskell. Programowanie refleksyjne – umożliwia pisanie programów samomodyfikujących się. Program może czytać własny kod, i go modyfikować. Języki: Python, Lisp, Scheme. Programowanie sterowane przepływem danych - programy wykonywane nie według ustalonej kolejności czynności, lecz według dostępności danych ( wykonywanie na nich czynności, gdy dane staną się dostępne). Przykład: praca arkusza kalkulacyjnego - przelicza dane, gdy tylko się zmienią oraz przetwarzanie potokowe w Uniksowych systemach operacyjnych. Języki: Linda. Programowanie współbieżne, równoległe, rozproszone – powiązane ze sobą (choć nietożsame) paradygmaty, bliskie programowaniu sterowanemu przepływem danych. Uwzględniają zagadnienia związane są z podziałem czasu procesora (lub procesorów) między procesy, synchronizacją procesów, podziałem pamięci wspólnej, przesyłaniem komunikatów pomiędzy procesami.