Polimorfizm (informatyka)

Z Wikipedii, wolnej encyklopedii
Skocz do: nawigacji, wyszukiwania
Ujednoznacznienie Ten artykuł dotyczy programowania. Zobacz też: inne znaczenia tego słowa.

Polimorfizm (z gr. wielopostaciowość) - mechanizmy pozwalające programiście używać wartości, zmiennych i podprogramów na kilka różnych sposobów[1]. Inaczej mówiąc jest to możliwość wyabstrahowania wyrażeń od konkretnych typów[2].

Przyczyny stosowania polimorfizmu[edytuj | edytuj kod]

Podczas pisania programu wygodnie jest traktować nawet różne dane w jednolity sposób. Niezależnie czy należy wydrukować liczbę czy napis, czytelniej (zazwyczaj) jest gdy operacja taka nazywa się po prostu drukuj, a nie drukuj_liczbę i drukuj_napis. Jednak napis musi być drukowany inaczej niż liczba, dlatego będą istniały dwie implementacje polecenia drukuj, ale nazwanie ich wspólną nazwą tworzy wygodny abstrakcyjny interfejs niezależny od typu drukowanej wartości.

Czasami nawet nie trzeba dostarczać różnych implementacji, przykładowo podczas implementacji stosu nie jest bardzo istotne, jakiego typu wartości będą na nim przechowywane. Można napisać ogólne algorytmy obsługujące stos i ewentualne ukonkretnienie pozostawić systemowi. Mechanizmy umożliwiające takie udogodnienia nazywane są właśnie polimorfizmem.

Polimorfizm statyczny i dynamiczny[edytuj | edytuj kod]

Wiele mechanizmów polimorficznych można napisać ręcznie, jednak wiąże się to często z koniecznością powielania kodu z jedynie niewielkimi poprawkami, a co za tym idzie rozrost kodu źródłowego i jego zaciemnienie. Istotą polimorfizmu jest to aby to system decydował o szczegółach, nie programista. Przez system należy tu rozumieć kompilator i system czasu wykonania.

Niektóre decyzje mogą być podjęte już na etapie kompilacji, mamy wtedy do czynienia z polimorfizmem statycznym (czasu kompilacji). Czasami jednak decyzja musi zostać odłożona do momentu wykonywania programu - polimorfizm dynamiczny (czasu wykonania). Przykładem statycznego może być przeciążanie operatorów - to którą wersję operatora należy wywołać można ustalić podczas kompilacji, natomiast dynamicznego - metody wirtualne - konkretna wersja metody może być ustalona dopiero w czasie wykonywania programu.

Polimorfizm uniwersalny[edytuj | edytuj kod]

Pozwala pisać ogólne struktury danych i algorytmy, bez precyzowania na jakich dokładnie typach one operują i bez konieczności dostarczania implementacji odpowiednich dla każdego przypadku.

Polimorfizm parametryczny[edytuj | edytuj kod]

Fragmenty programu mogą być parametryzowane typami lub po prostu nie precyzować typów danych na jakich operują. Przeważnie w językach bez inferencji typów wprowadza się zmienną typową (czyli zmienną przebiegającą przestrzeń typów, a nie wartości jakiegoś typu), która zastępuje nazwę typu będącego parametrem, w parametryzowanym fragmencie. W językach z inferencją, po prostu nie podaje się typu, a system sam wykrywa, że np. dana funkcja może operować na dowolnym typie i sygnalizuje to obecnością zmiennych typowych w wyznaczonym typie.

Przykładem mogą być następujące konstrukcje znane z języków imperatywnych:

  • szablony (ang. templates) w C++,
  • klasy, typy, funkcje i metody generyczne (ang. generics), np. w językach Java, C#, Ada.

W zależności od używanego języka specjalizacja uogólnionego algorytmu może być przeprowadzana automatycznie przez kompilator (C++), w innych językach (Ada) specjalizacja musi być podana jawnie, w jeszcze innych (Java) implementacja jest wspólna, a sprawdzane są jedynie deklarowane typy podczas kompilacji.

W językach funkcyjnych polimorfizm parametryczny jest bardzo często stosowany. Najprostszym przykładem funkcji polimorficznej jest identyczność:

let id = fun x=x

która jest typu \alpha\rightarrow\alpha, gdzie α jest zmienną typową. Funkcję można zaaplikować do argumentu dowolnego typu, a otrzymany wynik będzie tego samego typu co argument. Ciekawszym przypadkiem może być funkcja

let app = fun f x -> f x

typu (\alpha\rightarrow\beta)\rightarrow\alpha\rightarrow\beta, które przyjmuje jako parametry dowolną funkcję oraz wartość której typ zgadza się z typem wejściowym funkcji z pierwszego parametru; zwraca natomiast wartość typu zgodnego z wynikiem funkcji z pierwszego parametru.

Polimorfizm inkluzyjny[edytuj | edytuj kod]

Zakłada istnienie zwrotnej i przechodniej relacji (praporządku) na typach: τ<:ρ, mówimy wtedy, że τ jest podtypem (typem podrzędnym, ang. subtype) ρ, a ρ nadtypem (typem nadrzędnym, ang. supertype) τ. W miejscu gdzie oczekiwana jest wartość typu nadrzędnego, może być dostarczona wartość podtypu. Przykładem polimorfizmu inkluzyjnego mogą być podtypowanie i dziedziczenie, które w ogólnym przypadku tworzą dwie niezależne hierarchie.

Polimorfizm ograniczeniowy[edytuj | edytuj kod]

Rozszerzeniem zwykłego polimorfizmu parametrycznego jest możliwość nakładania pewnych ograniczeń na parametr typowy. Operacje wykonywane przez sparametryzowaną konstrukcję, mogą polegać na jakiejś właściwości danych na których pracują. Przykładowo implementacja homogenicznego zbioru, wymaga, aby można było sprawdzać równość przechowywanych obiektów, więc przykładowo parametr ogólnej implementacji może być ograniczony jedynie do klas posiadającym funkcję equals. Ograniczenia przeważnie polegają na wspomnianej już relacji "<:", czyli na byciu nadtypem lub podtypem, dlatego polimorfizm ograniczeniowy często traktowany jest jako połączenie polimorfizmów parametrycznego z inkluzyjnym.

Polimorfizm ad-hoc[edytuj | edytuj kod]

Pozwala dostarczyć kilku implementacji odpowiednich dla różnych typów ale połączyć je w jeden interfejs, następnie używać tego interfejsu, a wybór najbardziej odpowiedniej implementacji pozostawić systemowi (kompilatorowi, systemowi czasu wykonania). Najczęściej chodzi tu o wybór algorytmu odpowiedniego do typów danych.

Przeciążanie[edytuj | edytuj kod]

Przeciążanie (lub przeładowywanie, ang. overload) pozwala nazwać tak samo kilka podprogramów operujących na różnych danych i następnie obsługiwać te dane w jednolity sposób. Np. inaczej dodawane są liczby całkowite, a inaczej zmiennopozycyjne ale wygodnie obie te operacje nazywać po prostu dodawaniem i oznaczać symbolem "+". W językach bez przeciążania operatory te muszą się różnić (np. w ocamlu są oddzielne operatory "+" i "+."). Przeciążane mogą być np. operatory, funkcje, metody, procedury. W niektórych językach niektóre operatory lub funkcje są przeciążone, ale programista nie może ich dodatkowo dociążać lub przeciążać własnych.

Wybór konkretnego podprogramu następuje w miejscu wywołania na podstawie jego nazwy oraz statycznych typów argumentów, a czasami również na podstawie typu oczekiwanej wartości zwracanej (np. w Adzie). Jest to więc polimorfizm statyczny.

Multimetody[edytuj | edytuj kod]

Mechanizmem podobnym do przeciążania ale opierającym wybór na dynamicznym, a nie statycznym, typie argumentów są multimetody (ang. multimethods lub multiple dispatch). Metody takie można traktować jak metody wirtualne ale należące do kilku różnych klas. Mechanizm ten nie jest zbyt popularny, spotkać go można w języku CLOS. Z powodu opierania się na dynamicznym typie argumentów jest to polimorfizm dynamiczny.

Overriding with single dispatch[edytuj | edytuj kod]

Funkcje składowe obiektów mogą być traktowane tak jakby jednym z ich parametrów było odniesienie do obiektu na rzecz którego są wywoływane (this, self). Definiowanie explicite tego wskaźnika można znaleźć np. w Pythonie (tradycyjnie nazywany self). Gdy klasy pochodne mogą zastępować (ang. override) funkcje zdefiniowane w klasach nadrzędnych, a wybór wywoływanej funkcji dokonywany jest na podstawie dynamicznego typu obiektu na rzecz którego funkcja jest wywoływana, to również jest to polimorfizm dynamiczny. Mechanizm ten w wielu językach nazywany jest metodami lub funkcjami wirtualnymi.

Rekordy wariantowe[edytuj | edytuj kod]

Podczas wykonywania programu może zajść potrzeba różnego interpretowania wartości zawartych w pewnej zmiennej. Typowymi przykładami są:

  • Przetwarzanie wiadomości z jednolitym nagłówkiem, określającym typ wiadomości oraz zmiennym formatem treści, zależnym od typu.
  • Heterogeniczne struktury danych, np. drzewa przechowujące dane różnych typów.

Wygodnym rozwiązaniem są wtedy rekordy wariantowe. Zmienna takiego typu ma określony typ statyczny ale faktyczny format zawieranych danych (typ dynamiczny) może się zmieniać w trakcie wykonywania programu - polimorfizm dynamiczny.

Koercja, konwersja, rzutowanie[edytuj | edytuj kod]

Wszelkiego rodzaju zmiany typów, zarówno jawne jak i niejawne, statyczne i dynamiczne, również należy uznać za mechanizmy polimorficzne, gdyż pozwalają traktować wartości i zmienne jednego typu, tak jakby były wartościami innego typu.

Zobacz też[edytuj | edytuj kod]

Przypisy

  1. Mordechai Ben-Ari: Understanding Programming Languages. Chichester: John Wiley & Sons, 1996.
  2. Robert Harper: Type Systems for Programming Languages.

Bibliografia[edytuj | edytuj kod]

  1. Mordechai Ben-Ari: Understanding Programming Languages. Chichester: John Wiley & Sons, 1996.
  2. Bjarne Stroustrup: Język C++. Warszawa: WNT, 2000.
  3. Robert Harper: Practical Foundations for Programming Languages. 2008.