En la teoría del lenguaje de programación y la teoría de tipos , el polimorfismo es el uso de un solo símbolo para representar múltiples tipos diferentes. [1]
En programación orientada a objetos , el polimorfismo es la provisión de una única interfaz para entidades de diferentes tipos . [2] El concepto está tomado de un principio de biología según el cual un organismo o especie puede tener muchas formas o etapas diferentes. [3]
Las principales formas de polimorfismo más comúnmente reconocidas son:
El interés en los sistemas de tipos polimórficos se desarrolló significativamente en la década de 1990, y las implementaciones prácticas comenzaron a aparecer a finales de la década. El polimorfismo ad hoc y el polimorfismo paramétrico se describieron originalmente en Conceptos fundamentales en lenguajes de programación de Christopher Strachey , [5] donde se enumeran como "las dos clases principales" de polimorfismo. El polimorfismo ad hoc era una característica de Algol 68 , mientras que el polimorfismo paramétrico era la característica central del sistema de tipos de ML .
En un artículo de 1985, Peter Wegner y Luca Cardelli introdujeron el término polimorfismo de inclusión para modelar subtipos y herencia , [1] citando a Simula como el primer lenguaje de programación en implementarlo.
Christopher Strachey eligió el término polimorfismo ad hoc para referirse a funciones polimórficas que se pueden aplicar a argumentos de diferentes tipos, pero que se comportan de manera diferente según el tipo de argumento al que se aplican (también conocido como sobrecarga de funciones o sobrecarga de operadores ). [5] El término " ad hoc " en este contexto no pretende ser peyorativo; se refiere simplemente al hecho de que esta forma de polimorfismo no es una característica fundamental del sistema de tipos. En el siguiente ejemplo de Java , las Add
funciones parecen funcionar genéricamente en dos tipos (entero y cadena) cuando se analizan las invocaciones, pero el compilador las considera dos funciones completamente distintas para todos los efectos:
clase AdHocPolymorphic { public String add ( int x , int y ) { return "Suma: " + ( x + y ); } cadena pública agregar ( nombre de cadena ) { return "Agregado" + nombre ; } } public class adhoc { public static void main ( String [] args ) { AdHocPolymorphic poli = nuevo AdHocPolymorphic (); Sistema . afuera . println ( poli . agregar ( 1 , 2 ) ); // imprime "Suma: 3" Sistema . afuera . println ( poly.add ( " Jay " ) ); // imprime "Jay agregado" } }
En lenguajes de tipo dinámico la situación puede ser más compleja ya que es posible que la función correcta que debe invocarse solo pueda determinarse en tiempo de ejecución.
La conversión de tipos implícita también se ha definido como una forma de polimorfismo, denominada "polimorfismo de coerción". [1] [6]
El polimorfismo paramétrico permite escribir una función o un tipo de datos de forma genérica, de modo que pueda manejar valores de manera uniforme sin depender de su tipo. [7] El polimorfismo paramétrico es una forma de hacer que un lenguaje sea más expresivo manteniendo al mismo tiempo la seguridad de tipos estática completa .
El concepto de polimorfismo paramétrico se aplica tanto a tipos de datos como a funciones . Una función que puede evaluarse o aplicarse a valores de diferentes tipos se conoce como función polimórfica. Un tipo de datos que puede parecer de tipo generalizado (por ejemplo, una lista con elementos de tipo arbitrario) se denomina tipo de datos polimórfico como el tipo generalizado a partir del cual se realizan tales especializaciones.
El polimorfismo paramétrico es omnipresente en la programación funcional, donde a menudo se lo denomina simplemente "polimorfismo". El siguiente ejemplo en Haskell muestra un tipo de datos de lista parametrizada y dos funciones paramétricamente polimórficas sobre ellos:
Lista de datos a = Nil | Contras a ( Listar a ) longitud :: Lista a -> Longitud entera Nil = 0 longitud ( Cons x xs ) = 1 + longitud xs mapa :: ( a -> b ) -> Lista a -> Lista b mapa f Nil = Nil mapa f ( Cons x xs ) = Cons ( f x ) ( mapa f xs )
El polimorfismo paramétrico también está disponible en varios lenguajes orientados a objetos. Por ejemplo, plantillas en C++ y D, o bajo el nombre genéricos en C#, Delphi, Java y Go:
clase Lista < T > { clase Nodo < T > { T elem ; Nodo <T> siguiente ; } Cabeza del nodo <T> ; longitud int () { ... } } Lista < B > mapa ( Func < A , B > f , Lista < A > xs ) { ... }
John C. Reynolds (y más tarde Jean-Yves Girard ) desarrollaron formalmente esta noción de polimorfismo como una extensión del cálculo lambda (llamado cálculo lambda polimórfico o Sistema F ). Cualquier función paramétricamente polimórfica está necesariamente restringida en lo que puede hacer, trabajando en la forma de los datos en lugar de su valor, lo que lleva al concepto de parametricidad .
Algunos lenguajes emplean la idea de subtipos (también llamado polimorfismo de subtipo o polimorfismo de inclusión ) para restringir la gama de tipos que se pueden utilizar en un caso particular de polimorfismo. En estos lenguajes, la subtipificación permite escribir una función para tomar un objeto de un determinado tipo T , pero también funciona correctamente si se pasa un objeto que pertenece a un tipo S que es un subtipo de T (de acuerdo con el principio de sustitución de Liskov ). . Este tipo de relación a veces se escribe S <: T . Por el contrario, se dice que T es un supertipo de S , escrito T :> S . El polimorfismo de subtipo generalmente se resuelve dinámicamente (ver más abajo).
En el siguiente ejemplo de Java creamos subtipos de mascotas para gatos y perros. El procedimiento letsHear()
acepta una mascota, pero también funcionará correctamente si se le pasa un subtipo:
clase abstracta mascota { cadena abstracta hablar (); } clase Gato extiende Mascota { String hablar () { return "¡Miau!" ; } } clase Perro extiende Mascota { String hablar () { return "¡Guau!" ; } } static void letsHear ( final Pet pet ) { println ( pet . hablar ()); } static void main ( String [] args ) { letsHear ( new Cat ()); letHear ( nuevo Perro ()); }
En otro ejemplo, si Número , Racional y Entero son tipos tales que Número :> Racional y Número :> Entero ( Racional y Entero como subtipos de un tipo Número que es un supertipo de ellos), una función escrita para tomar un Número funcionan igual de bien cuando se les pasa un Integer o Rational que cuando se les pasa un Number . El tipo real de objeto se puede ocultar a los clientes en un cuadro negro y se puede acceder a él a través de la identidad del objeto . De hecho, si el tipo Número es abstracto , puede que ni siquiera sea posible conseguir un objeto cuyo tipo más derivado sea Número (ver tipo de datos abstracto , clase abstracta ). Este tipo particular de jerarquía de tipos se conoce, especialmente en el contexto del lenguaje de programación Scheme , como torre numérica y normalmente contiene muchos más tipos.
Los lenguajes de programación orientados a objetos ofrecen polimorfismo de subtipos mediante subclases (también conocida como herencia ). En implementaciones típicas, cada clase contiene lo que se llama una tabla virtual (abreviadamente llamada vtable ), una tabla de funciones que implementa la parte polimórfica de la interfaz de clase, y cada objeto contiene un puntero a la vtable de su clase, que luego se consulta. cada vez que se llama a un método polimórfico. Este mecanismo es un ejemplo de:
this
objeto), por lo que los tipos de tiempo de ejecución de los otros argumentos son completamente irrelevantes.Lo mismo ocurre con la mayoría de los otros sistemas de objetos populares. Algunos, sin embargo, como Common Lisp Object System , proporcionan despacho múltiple , bajo el cual las llamadas a métodos son polimórficas en todos los argumentos.
La interacción entre polimorfismo paramétrico y subtipificación conduce a los conceptos de varianza y cuantificación acotada .
El polimorfismo de filas [8] es un concepto similar, pero distinto, del subtipo. Se trata de tipos estructurales . Permite el uso de todos los valores cuyos tipos tienen ciertas propiedades, sin perder la información de tipo restante.
Un concepto relacionado es el politipismo (o genericidad del tipo de datos ). Una función politípica es más general que polimórfica, y en dicha función, "aunque se pueden proporcionar casos ad hoc fijos para tipos de datos específicos, no existe un combinador ad hoc". [9]
El polimorfismo de rango es una de las características definitorias de los lenguajes de programación de matrices , como APL . La esencia del modelo de programación polimórfica de rango es tratar implícitamente todas las operaciones como operaciones agregadas, utilizables en matrices con muchas dimensiones arbitrarias, [10] es decir, el polimorfismo de rango permite definir funciones para operar en matrices de cualquier forma y tamaño. .
El polimorfismo se puede distinguir según cuándo se selecciona la implementación: estáticamente (en tiempo de compilación) o dinámicamente (en tiempo de ejecución, generalmente a través de una función virtual ). Esto se conoce respectivamente como despacho estático y despacho dinámico , y las formas correspondientes de polimorfismo se denominan en consecuencia polimorfismo estático y polimorfismo dinámico .
El polimorfismo estático se ejecuta más rápido porque no hay una sobrecarga de despacho dinámico, pero requiere soporte adicional del compilador. Además, el polimorfismo estático permite un mayor análisis estático por parte de los compiladores (especialmente para optimización), herramientas de análisis de código fuente y lectores humanos (programadores). El polimorfismo dinámico es más flexible pero más lento; por ejemplo, el polimorfismo dinámico permite escribir pato y una biblioteca vinculada dinámicamente puede operar sobre objetos sin conocer su tipo completo.
El polimorfismo estático suele ocurrir en el polimorfismo ad hoc y el polimorfismo paramétrico, mientras que el polimorfismo dinámico es habitual en el polimorfismo de subtipo. Sin embargo, es posible lograr un polimorfismo estático con subtipos mediante un uso más sofisticado de la metaprogramación de plantillas , es decir, el patrón de plantilla curiosamente recurrente .
Cuando el polimorfismo se expone a través de una biblioteca , el polimorfismo estático se vuelve imposible para las bibliotecas dinámicas ya que no hay forma de saber de qué tipos son los parámetros cuando se construye el objeto compartido . Mientras que lenguajes como C++ y Rust utilizan plantillas monomorfizadas , el lenguaje de programación Swift hace un uso extensivo del envío dinámico para crear la interfaz binaria de la aplicación para estas bibliotecas de forma predeterminada. Como resultado, se puede compartir más código para un tamaño de sistema reducido a costa de una sobrecarga de tiempo de ejecución. [11]
Polimorfismo: proporciona una interfaz única para entidades de diferentes tipos.