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

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

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, aby to system decydował o szczegółach, a 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

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

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 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 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

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

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ących 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

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

Przeciążanie (lub przeładowywanie, ang. overloading) 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

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

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

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

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ż

Przypisy

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

Bibliografia

This article is issued from Wikipedia. The text is licensed under Creative Commons - Attribution - Sharealike. Additional terms may apply for the media files.