Polimorfism (inginerie software)

În teoria limbajelor de programare⁠(d) și a tipurilor⁠(d), polimorfismul este furnizarea unei singure interfețe de către entități de diferite tipuri,[1] sau utilizarea unui singur simbol pentru a reprezenta mai multe tipuri diferite.[2] Conceptul este împrumutat de la un principiu din biologie în care un organism sau o specie poate avea multe forme sau faze diferite.[3]

Clasele majore de polimorfism

Cele mai frecvent recunoscute clase majore de polimorfism sunt:

  • Polimorfismul ad-hoc⁠(d): se definește o interfață comună pentru un set arbitrar de tipuri specificate individual.
  • Polimorfism parametric⁠(d): când unul sau mai multe tipuri nu sunt specificate prin nume, ci prin simboluri abstracte care pot reprezenta orice tip.
  • Subtiparea⁠(d) (numită și polimorfism de subtip sau polimorfism de incluziune): atunci când un nume denotă instanțe ale mai multor clase diferite legate de o superclasă comună.[4]

Interesul pentru sistemele de tipare⁠(d) polimorfe s-a dezvoltat semnificativ în anii 1960, implementările practice începând să apară până la sfârșitul acelui deceniu. Polimorfismul ad-hoc și polimorfismul parametric au fost descrise inițial în Conceptele fundamentale în limbaje de programare⁠(d) ale lui Christopher Strachey,[5] unde sunt enumerate ca „cele două clase principale” de polimorfism. Polimorfismul ad-hoc era o caracteristică a limbajului Algol 68⁠(d), în timp ce polimorfismul parametric a fost caracteristica de bază a sistemului de tipare din ML⁠(d).

Într-o lucrare din 1985, Peter Wegner⁠(d) și Luca Cardelli⁠(d) au introdus termenul de polimorfism de incluziune pentru a modela subtipurile și moștenirea⁠(d),[2] dând Simula drept primul limbaj de programare care îl implementează.

Tipurile

modificare

Polimorfismul ad-hoc

modificare

Christopher Strachey a ales termenul de polimorfism ad-hoc cu referire la funcțiile polimorfe care pot fi aplicate argumentelor de diferite tipuri, dar care se comportă diferit în funcție de tipul argumentului căruia îi sunt aplicate (cunoscut și ca supraîncărcare a funcțiilor sau supraîncărcare a operatorilor).[5] Termenul „ad hoc” în acest context nu se dorește a fi peiorativ; se referă pur și simplu la faptul că acest tip de polimorfism nu este o trăsătură fundamentală a sistemului de tipare. În exemplul Pascal/Delphi de mai jos, funcțiile Add par să funcționeze generic pe diferite tipuri atunci când privim invocările, dar sunt considerate a fi două funcții complet distincte de către compilator:

program Adhoc;

function Add(x, y : Integer) : Integer;
begin
  Add := x + y
end;

function Add(s, t : String) : String;
begin
  Add := Concat(s, t)
end;

begin
  Writeln(Add(1, 2));          (* Prints "3"       *)
  Writeln(Add('Hello, ', 'Mammals!'));  (* Prints "Hello, Mammals!" *)
end.

În limbile tipate dinamic⁠(d), situația poate fi mai complexă, deoarece funcția care trebuie invocată se poate determina doar la momentul execuției.

Conversia implicită de tip⁠(d) a fost definită și ea ca o formă de polimorfism, denumită „polimorfism de constrângere”.[2][6]

Polimorfismul parametric

modificare

Polimorfismul parametric permite ca o funcție sau un tip de date să fie scris generic, astfel încât să poată gestiona valorile în mod uniform, fără a depinde de tipul lor.[7] Polimorfismul parametric este o modalitate de a face un limbaj mai expresiv, menținând în același timp un type-safety⁠(d) complet static.

Conceptul de polimorfism parametric se aplică atât tipurilor de date, cât și funcțiilor⁠(d). O funcție care poate evalua sau care se aplica unor valori de diferite tipuri este denumită funcție polimorfă. Un tip de date care poate părea un tip generalizat (de exemplu, o listă⁠(d) cu elemente de tip arbitrar) se numește tip de date polimorf ca și tipul generalizat din care sunt făcute astfel de specializări.

Polimorfismul parametric este omniprezent în programarea funcțională, unde este adesea numit pur și simplu „polimorfism”. Următorul exemplu din Haskell prezintă o listă parametrizată și două funcții polimorfe parametric definite pe acestea:

data List a = Nil | Cons a (List a)

length :: List a -> Integer
length Nil     = 0
length (Cons x xs) = 1 + length xs

map :: (a -> b) -> List a -> List b
map f Nil     = Nil
map f (Cons x xs) = Cons (f x) (map f xs)

Polimorfismul parametric este disponibil și în mai multe limbaje orientate pe obiecte. De exemplu, template-urile din C++ și D, sau sub genericele⁠(d) din C#, Delphi, Java și Go:

class List<T> {
  class Node<T> {
    T elem;
    Node<T> next;
  }
  Node<T> head;
  int length() { ... }
}

List<B> map(Func<A, B> f, List<A> xs) {
  ...
}

John C. Reynolds⁠(d) (și mai târziu Jean-Yves Girard⁠(d)) au dezvoltat formal această noțiune de polimorfism ca o extensie a calculului lambda (numit calcul lambda polimorf sau System F⁠(d)). Orice funcție polimorfă parametric are automat unele restricții asupra a ceea ce poate face ea: poate lucra doar asupra formei datelor, și nu pe valoarea acestora, ceea ce conduce la conceptul de parametricitate.

Subtiparea

modificare

Unele limbaje folosesc ideea de subtipare (numită și polimorfism de subtip sau polimorfism de incluziune) pentru a restricționa gama de tipuri care pot fi utilizate într-un anumit caz de polimorfism. În aceste limbaje, subtiparea permite scrierea unei funcții în așa fel încât primește ca parametru un obiect de un anumit tip T, dar ar funcționa corect și dacă primește un obiect care aparține unui tip S care este un subtip al lui T (conform principiului de substituție Liskov⁠(d)). Această relație între tipuri se scrie uneori S < : T. Invers, se spune că T este un supertip al lui S — scris T : > S. Polimorfismul de subtip este de obicei rezolvat dinamic (vezi mai jos).

În următorul exemplu Java, pisicile și câinii sunt subtipuri ale tipului animal. Procedura letsHear() acceptă un animal, dar va funcționa corect și dacă îi este transmis un subtip:

abstract class Animal {
  abstract String talk();
}

class Cat extends Animal {
  String talk() {
    return "Meow!";
  }
}

class Dog extends Animal {
  String talk() {
    return "Woof!";
  }
}

static void letsHear(final Animal a) {
  println(a.talk());
}

static void main(String[] args) {
  letsHear(new Cat());
  letsHear(new Dog());
}

Într-un alt exemplu, dacă Number, Rational și Integer sunt tipuri astfel încât Number : > Rational și Number : > Integer, o funcție care primește un Number va functiona la fel de bine atunci cand trece un Integer sau un Rational ca și atunci cand primește un Number. Tipul real al obiectului poate fi ascuns clienților într-o cutie neagră⁠(d) și accesat prin identitatea⁠(d) obiectului. De fapt, dacă tipul Number este abstract, s-ar putea să nu fie nici măcar posibilă primirea unui obiect al cărui tip cel mai derivat este Number. Acest tip special de ierarhie de tipuri este cunoscut – mai ales în contextul limbajului de programare Scheme – drept turn numeric⁠(d) și, de obicei, conține mai multe tipuri.

Limbajele de programare orientate pe obiecte oferă polimorfism de subtipuri folosind subclasarea⁠(d) (cunoscută și sub numele de moștenire⁠(d)). În implementările tipice, fiecare clasă conține ceea ce se numește tabela virtuală⁠(d) — o tabelă de funcții care implementează partea polimorfă a interfeței clasei — și fiecare obiect conține un pointer către tabela virtuală a clasei sale, care este apoi consultat ori de câte ori se apelează o metodă polimorfă. Acest mecanism este un exemplu de:

  • Late binding⁠(d), deoarece apelurile de funcții virtuale nu sunt legate până la momentul invocării;
  • single dispatch⁠(d) (adică polimorfism cu un singur argument), deoarece apelurile de funcții virtuale sunt legate pur și simplu prin căutarea prin tabela virtuală furnizată de primul argument (obiectul this ), astfel încât tipurile celorlalte argumente de la momentul rulări sunt complet irelevante.

Același lucru este valabil și pentru majoritatea celorlalte sisteme de obiecte. Unele, totuși, cum ar fi Common Lisp Object System⁠(d), oferă Multiple dispatch⁠(d), în care apelurile de metodă sunt polimorfe în toate argumentele.

Interacțiunea dintre polimorfismul parametric și subtipare duce la conceptele de varianță⁠(d) și Bounded quantification⁠(d).

Polimorfismul de rând

modificare

Polimorfismul de rând[8] este un concept similar, dar distinct de subtipare. Se ocupă de tipuri structurale⁠(d). Permite utilizarea tuturor valorilor ale căror tipuri au anumite proprietăți, fără a pierde informațiile de tip rămase.

Aspecte de implementare

modificare

Polimorfism static și dinamic

modificare

Polimorfismul poate fi clasificat și după momentul în care este selectată implementarea: static (la compilare) sau dinamic (la rulare, de obicei printr-o funcție virtuală). Aceasta este cunoscută, respectiv, sub denumirea de Static dispatch⁠(d) și Dynamic dispatch⁠(d), iar formele corespunzătoare de polimorfism sunt denumite, în consecință, polimorfism static și polimorfism dinamic.

Polimorfismul static se execută mai rapid, deoarece nu există un overhead de dynamic dispatch, dar necesită suport suplimentar în compilator. În plus, polimorfismul static permite o analiză statică mai mare a compilatarelor (în special pentru optimizare), instrumente de analiză a codului sursă și cititori umani (programatori). Polimorfismul dinamic este mai flexibil, dar mai lent — de exemplu, polimorfismul dinamic permite duck typing, iar o bibliotecă legată dinamic poate funcționa pe obiecte fără a cunoaște tipul lor complet.

Polimorfismul static apare de obicei în polimorfismul ad-hoc și în polimorfismul parametric, în timp ce polimorfismul dinamic este obișnuit pentru polimorfismul de subtip. Cu toate acestea, se poate obține polimorfism static cu subtipare prin utilizarea mai sofisticată a metaprogramării cu template-uri⁠(d), și anume CRTP⁠(d).

Când polimorfismul este expus printr-o bibliotecă⁠(d), polimorfismul static devine imposibil pentru bibliotecile dinamice,⁠(d) deoarece nu există nicio modalitate de a ști ce tipuri au parametrii la momentul când este construit shared objectul. În timp ce limbaje precum C++ și Rust folosesc template-uri monomorfizate⁠(d), limbajul de programare Swift utilizează în mod implicit dynamic dispatch pentru a construi interfața binară de aplicație⁠(d) pentru aceste biblioteci. Ca rezultat, se poate partaja mai mult cod pentru o dimensiune redusă a sistemului cu costul unui overhead la rulare.[9]

  1. ^ Bjarne Stroustrup (). „Bjarne Stroustrup's C++ Glossary”. polymorphism – providing a single interface to entities of different types. 
  2. ^ a b c Cardelli, Luca; Wegner, Peter (decembrie 1985). „On understanding types, data abstraction, and polymorphism” (PDF). ACM Computing Surveys⁠(d). 17 (4): 471–523. doi:10.1145/6041.6042. : "Polymorphic types are types whose operations are applicable to values of more than one type."
  3. ^ „Polymorphism”. The Java™ Tutorials: Learning the Java Language: Interfaces and Inheritance. Oracle. Accesat în . 
  4. ^ Conallen, J.; Engle, M.; Houston, K.; Maksimchuk, R.; Young, B.; Booch, G. (). Object-Oriented Analysis and Design with Applications (ed. 3rd). Pearson Education. ISBN 9780132797443. 
  5. ^ a b Strachey, Christopher (). „Fundamental Concepts in Programming Languages”. Higher-Order and Symbolic Computation⁠(d). 13 (1/2): 11–49. doi:10.1023/A:1010000313106. ISSN 1573-0557. 
  6. ^ Tucker, Allen B. (). Computer Science Handbook (ed. 2nd). Taylor & Francis. pp. 91–. ISBN 978-1-58488-360-9. 
  7. ^ Pierce, B.C. (). „23.2 Varieties of Polymorphism”. Types and Programming Languages. MIT Press. pp. 340–1. ISBN 9780262162098. 
  8. ^ Wand, Mitchell (iunie 1989). „Type inference for record concatenation and multiple inheritance”. Proceedings. Fourth Annual Symposium on Logic in Computer Science. pp. 92–97. doi:10.1109/LICS.1989.39162. 
  9. ^ Beingessner, Alexis. „How Swift Achieved Dynamic Linking Where Rust Couldn't”.