Opracował: mgr inŝ. Zbigniew JANIK Strona 1/7 PARADYGMATY PROGRAMOWANIA I Wstęp Co znaczy uŝyte w nazwie wykładu słowo paradygmat? Pochodzi z języka greckiego, w którym słowo παραδειγµα (paradeigma) oznacza wzorzec lub przykład. Internetowy słownik języka polskiego definiuje w wyraŝeniu paradygmaty programowania nie chodzi o wzorcowy sposób programowania czy teŝ o przykłady poprawnych programów. Mówiąc o paradygmatach programowania, mamy na myśli raczej zestaw typowych dla danej grupy języków mechanizmów udostępnionych programiście oraz zbiór sposobów interpretacji tych mechanizmów przez semantykę języka. Czyli jak rzeczywistość (tak zewnętrzna, opisywanego świata, jak i wewnętrzna, maszynowa) jest postrzegana przez pryzmat danego języka. Wszystkie języki programowania moŝna podzielić na dwie główne grupy: imperatywne, deklaratywne. Języki imperatywne charakteryzują się tym, Ŝe programista wyraŝa w nich czynności (w postaci rozkazów), które komputer ma w pewnej kolejności wykonywać. Program jest wiec listą rozkazów do wykonania, stad nazwa imperatywne ( rozkazowe ). W językach deklaratywnych jest inaczej. Tutaj programista pisząc program podaje (deklaruje) komputerowi pewne zaleŝności oraz cele, które program ma osiągnąć. Nie podaje się jednak wprost sposobu osiągnięcia wyników. Innymi słowy, języki imperatywne mówią komputerowi, jak ma osiągnąć wynik, (choć samego wyniku nie określają wprost), natomiast języki deklaratywne opisują, co ma być osiągnięte, (choć nie podają na to bezpośredniego sposobu). Ten podział przedstawia Rysunek 1. Rysunek 1. Podział głównych paradygmatów programowania Na rysunku 1 widać takŝe dalszy, drobniejszy, podział wspomnianych wyŝej dwóch głównych paradygmatów programowania. W programowaniu imperatywnym jak wyŝej to wspomnieliśmy program jest po prostu listą instrukcji (mniej lub bardziej elementarnych), które maja być wykonywane kolejno. JednakŜe, siła maszyn liczących jest umiejętność wielokrotnego powtarzania pewnych czynności zapisanych raz, czyli wykonywania pętli. śeby było to moŝliwe, musimy mieć do dyspozycji rozkaz zaburzający zapisaną sekwencje poleceń, a podstawową i pierwotnie stosowaną, pełniącą tę funkcję instrukcją jest rozkaz!skoku. Zestaw instrukcji zawierający elementarne rozkazy operacji na pewnych danych wraz z rozkazami skoków bezwarunkowych i warunkowych stanowi trzon kaŝdego z kodów maszynowych. Jest to teŝ najbardziej naturalny i pierwotny język (czy teŝ rodzina języków: kody maszynowe) dla wszelkiego rodzaju komputerów (i podobnych maszyn), bowiem są one budowane począwszy od ich powstania do dziś na bazie abstrakcyjnej architektury von Neumanna (lub architektur blisko pokrewnych).
Opracował: mgr inŝ. Zbigniew JANIK Strona 2/7 Architektura von Neumanna zakłada w uproszczeniu, Ŝ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 powoduje zmianę stanu maszyny rozumianego, jako zawartość całej pamięci włącznie z rejestrami i znacznikami procesora; rozkazy mogą wiec zmieniać wewnętrzne ustawienie jednostki centralnej, w tym miejsce, z którego biedzie czytany następny rozkaz. W praktyce komputery budowane są (i działają) właśnie tak (choć z pewnymi drobnymi modyfikacjami). W związku z tym trzeba wyraźnie powiedzieć, Ŝe Rysunek 2. Architektura von Neumanna najbardziej naturalnym paradygmatem dla maszyny jest paradygmat imperatywny. Innymi słowy: maszyna musi dostać kolejne kroki do wykonania, Ŝeby mogła coś zrobić. Z drugiej strony, dla człowieka duŝo wygodniejszym sposobem komunikowania poleceń jest podanie, co ma być osiągnięte, bez wdawania się w szczegóły wykonania a wiec paradygmat deklaratywny. System komputerowy zbudowany w oparciu o architekturę von Neumanna powinien: mieć skończoną i funkcjonalnie pełną listę rozkazów mieć moŝliwość wprowadzenia programu do systemu komputerowego poprzez urządzenia zewnętrzne i jego przechowywanie w pamięci w sposób identyczny jak danych dane i instrukcje w takim systemie powinny być jednakowo dostępne dla procesora informacja jest tam przetwarzana dzięki sekwencyjnemu odczytywaniu instrukcji z pamięci komputera i wykonywaniu tych instrukcji w procesorze. Podane warunki pozwalają przełączać system komputerowy z wykonania jednego zadania (programu) na inne bez fizycznej ingerencji w strukturę systemu, a tym samym gwarantują jego uniwersalność. Tabela 1. Języki programowania a główne paradygmaty Języki asemblery, stary BASIC, stary Fortran imperatywny stary Pascal, C C++, Object Pascal, Ada Smalltalk, C#, Java Lisp, Scheme, Logo, ML, OCaml Haskell Planner, Prolog Python, Ruby SQL Paradygmaty 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)
Opracował: mgr inŝ. Zbigniew JANIK Strona 3/7 Inne paradygmaty Warto na koniec wymienić jeszcze kilka bardziej niszowych paradygmatów, o których jednak wypadałoby słyszeć. Większość z poniŝszych to miniparadygmaty, w tym sensie, ze raczej współistnieją z obszerniejszymi paradygmatami (jak wymienione poprzednio) niŝ same stanowią rdzeń jakiegokolwiek języka programowania. Programowanie modularne jest pośrednie miedzy programowaniem obiektowym a proceduralnym. W tym paradygmacie główna jednostka planowania programu i jego tworzenia jest moduł (pakiet) zawarty zwykle w osobnym pliku i w wielu aspektach traktowany jako obiekt. Przykłady języków: Ada, Haskell, Python. Programowanie aspektowe jest blisko związane z paradygmatem modularnym, bowiem jego celem jest ścisły podział problemu na jak najbardziej niezaleŝne logicznie części i ograniczenie ich liczby styków oraz ścisłe kontrolowanie kaŝdego z nich. Przykłady języków: AspectJ. Programowanie komponentowe to kolejny paradygmat związany z modularyzacją programów, a jednocześnie z programowaniem obiektowym. Tutaj komponenty to jak najbardziej samodzielne obiekty wyposaŝone w ściśle wyspecyfikowany interfejs, wykonujące pewne określone usługi. Zwykle paradygmat ten związany jest ściśle z programowaniem zdarzeniowym. Przykłady języków: Eiffel, Oberon. Programowanie agentowe moŝna uznać za nieco bardziej abstrakcyjną formę programowania obiektowego. Tutaj jednostką podstawowa jest oczywiście agent, czyli wyspecjalizowany i odporny na błędy i niepowodzenia, a jednocześnie samodzielny obiekt, który w pewnym środowisku (często rozległym lub heterogenicznym, jak siec komputerowa) moŝe pracować sam, a w potrzebie komunikować sie 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 tez ewentualna moŝliwość samo replikacji agentów w odpowiednim środowisku i warunkach. Przykłady języków: JADE (Framework Javy). Programowanie zdarzeniowe (inaczej: sterowane zdarzeniami ) to takie, gdy program składa sie 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ń. Oprócz wspomnianych związków tego paradygmatu z komponentowym, obiektowym czy agentowym widać go w systemach operacyjnych, które działają praktycznie w oparciu o ten paradygmat. W reszcie, obsługa wyjątków w róŝnych językach ma charakter programowania zdarzeniowego. Programowanie kontraktowe jest ściśle związane z paradygmatem obiektowym (ale mogłoby mieć równieŝ swoje miejsce jako rozszerzenie programowania strukturalnego) i polega na takim pisaniu kodu, by mógł być on automatycznie sprawdzony (pod względem zgodności ze specyfikacją) i ewentualnie przetestowany. Przykłady języków: Eiffel, interfejsy w Javie. Programowanie generyczne (inaczej: uogólnione, rodzajowe) umoŝliwia tworzenie jednostek (klas, obiektów, funkcji, typów) parametrycznych, lub inaczej mówiąc polimorficznych, uogólnionych, które staja sie pełnoprawnymi jednostkami w chwili ich dookreślenia, co moŝe zostać odłoŝone do momentu skorzystania z ich definicji w gotowym programie. Przykłady języków: Ada, C++, Haskell. Programowanie refleksyjne pozwala na pisanie programów samo modyfikujących się. Oznacza to, Ŝe program sam moŝe oglądać własny kod, ale co waŝniejsze, moŝe tez go modyfikować. Przykładem dość popularnego języka posiadającego mechanizm refleksji jest Python. Inne przykłady takich języków: Lisp, Scheme. Programowanie sterowane przepływem danych polega na konstruowaniu programów nie w oparciu o ustalona kolejność czynności, lecz o dostępność danych i wykonywanie na nich czynności w czasie, gdy dane staną się dostępne. Przykłady tego paradygmatu to praca arkusza kalkulacyjnego (który przelicza dane, gdy tylko sie zmienia) oraz przetwarzanie potokowe dobrze znane z Uniksowych systemów operacyjnych. Przykłady języków: Linda. Programowanie współbieŝne, równoległe, rozproszone to trzy ściśle ze sobą związane (choć nietoŝsame) paradygmaty, bliskie takŝe programowaniu sterowanemu przepływem danych. Problemy tego paradygmatu związane są z podziałem czasu jednostki wykonującej rozkazy (lub jednostek) pomiędzy procesy, synchronizacja procesów, synchronizacja ich dostępów do pamięci wspólnej, przesyłaniem komunikatów pomiędzy procesami.
Opracował: mgr inŝ. Zbigniew JANIK Strona 4/7 Składowe języka Trzy podstawowe elementy kaŝdego języka to: syntaktyka semantyka pragmatyka Syntaktyka (składnia) to opis reguł konstrukcji języka pozwalających na zbudowanie wszystkich zdań uznanych za poprawne dla tego języka. Reguły syntaktyczne moŝemy podzielić na: formujące pozwalają określić sposoby poprawnego tworzenia wyraŝeń transformujące pozwalają określić sposoby poprawnego przekształcania wyraŝeń tak, aby zachowana była pewna wyróŝniona własność wyraŝenia. Semantyka to opis znaczenia przypisywanego zdaniom poprawnym, czyli odniesienie wygenerowanych zdań do rzeczywistości. Semantyka bada takŝe relacje pomiędzy podstawowym znaczeniem wyrazu a jego uŝyciem w konkretnej wypowiedzi. Dla danego języka jako zbioru zdań uznanych za poprawne moŝemy przygotować wiele róŝnych opisów znaczeń. Pragmatyka to zespół reguł i zaleceń mówiących o tym, jak stosować składnię i semantykę języka. Bada relacje pomiędzy językiem i jego uŝytkownikami, pozwala na tworzenie róŝnych form lingwistycznych o zbliŝonym znaczeniu semantycznym i mówi, które z tych form lepiej jest wybrać. Innymi słowy, pragmatyka bada sposoby posługiwania się językiem przez ludzi, w szczególności rozumienie i interpretowanie wypowiedzi w zaleŝności od kontekstu, w którym są uŝyte. Składnia i semantyka Składnia to zbiór reguł, mówiących jak wygląda poprawny program w danym języku, czyli np.: Jak tworzy się polecenia i wyraŝenia. Jaką postać mają struktury sterowania (if, while, for itp.). Jak zapisuje się deklaracje. Jak widać z powyŝszego wyliczenia, jesteśmy tak przywiązani do języków imperatywnych i obiektowych, Ŝe chyba nie bardzo wyobraŝamy sobie język bez poleceń, pętli while, zmiennych... Semantyka to znaczenie wspomnianych wyŝej form, czyli co one robią. Weźmy dla przykładu typową instrukcję warunkową Składnia moŝe wyglądać tak: if "(" wyraŝenie ")" instrukcja Semantyka jest następująca: sprawdź podane wyraŝenie i jeśli jest prawdziwe, to wykonaj podaną instrukcję. Algorytmy programy i dane Na początku wprowadzimy kilka pojęć. Większość z nich jest juŝ prawdo-podobnie Wam znana, ale na wszelki wypadek zapiszę je tu teraz. Algorytmem nazywamy metodę rozwiązania danego problemu. ZauwaŜcie, Ŝe w powyŝszej definicji nie występuje ani słowo o komputerach i programowaniu. Algorytm jest pojęciem bardzo ogólnym, dlatego teŝ algorytmem jest np. przepis w ksiąŝce kucharskiej: Rozgrzej patelnię z odrobiną oleju lub - jeśli wolisz - masła, weź dwa jajka, wbij je na patelnię, dodaj szczyptę soli, mieszając smaŝ, aŝ się nie zetną - i ciesz się jajecznicą. Aby rozwiązać jakiś problem, wykonujemy kolejno pewne czynności, które nazywać będziemy krokami. Krokiem moŝe być zarówno bardzo prosta czynność (wbij jajka), nazywana w programowaniu operacją elementarną, jak i całkiem złoŝona sekwencja prostych czynności, którą będziemy nazywali podprogramem. Podprogramem "kulinarnym" jest na przykład przepis na wykonanie kremu, "wywoływany" w róŝnych wersjach (zrób krem czekoladowy, zrób krem waniliowy itp.) w wielu przepisach na torty. Istotną cechą algorytmu jest jego ogólność, co oznacza, Ŝe nie podaje on metody rozwiązania pojedynczego problemu, lecz całej grupy problemów podobnych (podany algorytm dotyczy zrobienia dowolnej jajecznicy). KaŜdy algorytm moŝna zapisać na wiele sposobów. Występują następujące sposoby przedstawiania algorytmów: 1. Słowny w języku naturalnym - na ogół mało dokładny. 2. Lista kroków. 3. Schemat blokowy. 4. Drzewo algorytmu.
Opracował: mgr inŝ. Zbigniew JANIK Strona 5/7 Podany powyŝej przykład został zapisany w języku naturalnym. JednakŜe jak się zapewne domyślacie, przydatność zapisu w języku naturalnym dla komputera jest (przynajmniej na razie) Ŝadna. PoniewaŜ komputery nie myślą, wymagają opisu całkowicie sformalizowanego, aŝ do najdrobniejszych szczegółów - i tym są właśnie języki programowania. Język programowania, podobnie jak język naturalny, takŝe ma swoje słownictwo i składnię. Oczywiście, są one duŝo, duŝo uboŝsze niŝ w językach naturalnych. Teraz juŝ moŝemy wprowadzić definicję programu (w domyśle: programu komputerowego, przeznaczonego do wykonania przez komputer): Programem nazywamy zapis algorytmu w danym języku programowania. Programowanie jest więc procesem zapisu algorytmu na uŝytek komputera; często mówi się, Ŝe programowanie to kodowanie algorytmu. W takim pojęciu osoba pisząca program, czyli programista, nie musi być autorem algorytmu. Częściej jednak termin programowanie oznacza równieŝ sam proces tworzenia algorytmu. A więc programowanie to zazwyczaj nie tylko technika czy inŝynieria programowania, ale równieŝ umiejętność czy wręcz sztuka programowania. KaŜdy algorytm operuje na pewnych danych. W przykładzie z jajecznicą danymi są np. jajka. Algorytm opisuje sposób przetwarzania danych wejściowych (jajka) w celu otrzymania Ŝądanych wyników, czyli danych wyjściowych (jajecznica). Musicie zrozumieć róŝnicę pomiędzy danymi a algorytmem na nich operującym. JuŜ wkrótce zaczniecie pisać pierwsze programy i łatwiej będzie Wam zrozumieć jak one działają, jeśli będziecie pamiętali o wyraźnym rozróŝnieniu pomiędzy danymi a programem. Podkreślmy to wyraźnie: wszystkie dane są w pełni niezaleŝne od programu na nich operującego. Uniwersalność algorytmu i wynikającego zeń programu polega na tym, Ŝe te same polecenia moŝna wykonywać na róŝnych danych zapisanych pod tymi samymi nazwami Mówiąc o danych zwykle myślimy o danych wejściowych, które są pobierane przez komputer w trakcie wykonywania programu; ale jak juŝ wiemy, dane w szerszym pojęciu to równieŝ dane wyjściowe, czyli wyniki generowane przez program. Kod źródłowy, maszynowy i kompilator Kompilacja i wykonanie programu Musimy teraz wyraźnie powiedzieć, Ŝe poziom instrukcji w językach wyŝszego poziomu bardzo róŝni się od poziomu instrukcji zrozumiałych bezpośrednio przez komputer, a ściślej rzecz biorąc przez jego procesor. Procesor rozumie zupełnie inne instrukcje, niŝ te, które dotąd poznaliśmy - tzw. rozkazy maszynowe. O programach i ich rozumieniu przez komputer moŝemy więc myśleć na dwóch poziomach abstrakcji: komputer "rozumie" program napisany w języku wyŝszego poziomu - czyli tzw. program źródłowy, zwany często kodem źródłowym; procesor "rozumie" program napisany w języku procesora - czyli tzw. program lub inaczej kod maszynowy (który jest kompletnie nieczytelny dla większości ludzi)
Opracował: mgr inŝ. Zbigniew JANIK Strona 6/7 Aby więc komputer mógł wykonać program napisany w języku źródłowym, musi on zostać jeszcze przetłumaczony na postać zrozumiałą dla procesora, czyli kod maszynowy. Tłumaczenie to moŝe być wykonywane dwojako: przed wykonaniem programu - wtedy proces ten nazywamy kompilacją, a specjalny program dokonujący tłumaczenia - kompilatorem;. na bieŝąco, w trakcie pracy programu - wtedy mamy do czynienia z interpretacją i interpreterem. Obie metody mają swoje wady i zalety, my będziemy wykorzystywali kompilator języka. Zatem Wy, jako programiści, najpierw zapisujecie opracowany algorytm w języku programowania, tworząc tzw. program źródłowy (lub inaczej: kod źródłowy), a następnie nakazujecie kompilatorowi przetłumaczenie go do postaci kodu wynikowego (maszynowego), wykonywanego przez procesor. I jedna i druga postać jest programem, tak więc podczas tego kursu mówiąc ogólnie o programie wykonywanym przez komputer nie będziemy dokonywali rozróŝnienia pomiędzy jego wersją źródłową a maszynową. Popatrzcie uwaŝnie na schemat: Ŝeby program wykonać, trzeba go najpierw skompilować. Jeśli program tylko skompilujemy, ale go nie wykonamy, to nie otrzymamy wyników. Kompilacja programu i wykonanie programu to są dwa osobne procesy. Często mówiąc o wykonaniu programu źródłowego mamy na myśli oba te procesy kolejno: kompilację, a potem wykonanie. KaŜdy z tych procesów moŝe prowadzić do błędów. MoŜemy więc mówić o dwu rodzajach błędów, które mogą się nam przytrafić, a ściślej komputerowi, który próbuje sobie poradzić z naszym programem: błędy kompilacji wynikają z tego, Ŝe kompilator (nasz program tłumaczący) nie rozumie programu źródłowego, bo są w nim błędy składniowe lub ortograficzne, na przykład brakuje nawiasu zamykającego albo napisaliśmy "bgin" zamiast "begin" (znacznie częściej zdarza się "lenght" zamiast "length") błędy wykonania wynikają stąd, Ŝe program źródłowy nie daje się wykonać (mimo Ŝe jest poprawny pod względem formalnym) - na przykład nie moŝe podzielić przez daną, która jest zerem, albo wyznaczyć pierwiastka kwadratowego z danej ujemnej. Błędy wykonania są znacznie trudniejsze do wykrycia, niŝ błędy kompilacji. Najtrudniejsze zaś do wykrycia są błędy, które w ogóle nie są sygnalizowane przez kompilator, czyli: błędy logiczne algorytmu, które powstają w procesie tworzenia algorytmu - wówczas program daje się wykonać, ale robi to źle. Czym jest zintegrowane środowisko programistyczne Po uruchomieniu IDE Delphi ujrzycie na ekranie zintegrowane środowisko programistyczne. Nie jest celem tego wykładu jego dokładne omawianie, niemniej jednak skrótowo opiszemy je tutaj. Aby pisać jakiekolwiek programy, potrzebujecie co najmniej narzędzi wymienionych poniŝej:
Opracował: mgr inŝ. Zbigniew JANIK Strona 7/7 Edytor - w końcu gdzieś tekst programu trzeba wprowadzić. Plik z tekstem programu ma strukturę zwykłego pliku tekstowego. MoŜna więc wykorzystywać dowolny edytor tekstowy, zaczynając od Notatnika, a kończąc na Wordzie (pod warunkiem, Ŝe pliki będziemy zapisywali w formacie tekstowym, a nie w typowym formacie Worda). Jednak do programowania wykorzystuje się zwykle specjalizowane edytory ułatwiające pracę programiście. Najbardziej oczywistym z ułatwień jest podświetlanie składni danego języka. Na pewno zauwaŝyliście, Ŝe zamieszczone wcześniej programy są sformatowane w pewien specyficzny sposób. Pewne słowa są wytłuszczone, komentarze pisane są kursywą, niektóre bloki zaczynają się od spacji itd. Taki a nie inny zapis programu ułatwia jego czytanie przez człowieka (dla komputera jest to zupełnie obojętne). W dalszej części kursu opiszemy dokładniej, jakich reguł powinniście się trzymać podczas formatowania tekstu programu. Edytory programistyczne kolorują słowa i konstrukcje danego języka i pomagają w prawidłowym stosowaniu wcięć. Typowymi edytorami do zadań programistycznych są dla Windows edytory wbudowane w Visual Studio firmy Microsoft czy teŝ edytory stosowane w kompilatorach Borlanda, dla Linuxa zaś edytory Emacs czy Nedit Kompilator lub interpreter czyli tłumacz. Jak juŝ wcześniej wspominaliśmy, słuŝą one do przetłumaczenia zapisu algorytmu w danym języku programowania na kod maszynowy moŝliwy do wykonania przez procesor. W przypadku wykorzystywania kompilatora potrzebny jest jeszcze linker zwany bardziej poprawnie konsolidatorem. Jest on pewnego rodzaju "pomocnikiem" kompilatora. Na tym poziomie powinna wystarczyć Wam informacja, Ŝe wynik pracy kompilatora musi zostać jeszcze dodatkowo przetworzony przez konsolidator, aby uzyskać działający program. Obrazuje to następujący schemat: Konsolidator słuŝy do połączenia wyników pracy kompilatora z bibliotekami wykorzystywanych procedur i zapis w formie bezpośrednio wykonywanego programu.