La programación genérica es un estilo de programación informática en el que los algoritmos se escriben en términos de tipos de datos que se especificarán más adelante y que luego se instancian cuando es necesario para tipos específicos proporcionados como parámetros . Este enfoque, iniciado por el lenguaje de programación ML en 1973, [1] [2] permite escribir funciones o tipos comunes que difieren solo en el conjunto de tipos en los que operan cuando se usan, lo que reduce el código duplicado .
La programación genérica se introdujo en el mundo de la programación convencional con Ada en 1977. Con las plantillas en C++ , la programación genérica se convirtió en parte del repertorio de diseño de bibliotecas profesionales. Las técnicas se mejoraron aún más y se introdujeron los tipos parametrizados en el influyente libro de 1994 Design Patterns . [3]
Andrei Alexandrescu introdujo nuevas técnicas en su libro de 2001 Modern C++ Design: Generic Programming and Design Patterns Applied . Posteriormente, D implementó las mismas ideas.
Estas entidades de software se conocen como genéricos en Ada , C# , Delphi , Eiffel , F# , Java , Nim , Python , Go , Rust , Swift , TypeScript y Visual Basic .NET . Se conocen como polimorfismo paramétrico en ML , Scala , Julia y Haskell . (La terminología de Haskell también utiliza el término "genérico" para un concepto relacionado pero algo diferente).
El término programación genérica fue acuñado originalmente por David Musser y Alexander Stepanov [4] en un sentido más específico que el anterior, para describir un paradigma de programación en el que los requisitos fundamentales de los tipos de datos se abstraen de ejemplos concretos de algoritmos y estructuras de datos y se formalizan como conceptos , con funciones genéricas implementadas en términos de estos conceptos, típicamente utilizando mecanismos de genericidad del lenguaje como los descritos anteriormente.
La programación genérica se define en Musser y Stepanov (1989) de la siguiente manera:
La programación genérica se centra en la idea de hacer abstracción de algoritmos concretos y eficientes para obtener algoritmos genéricos que puedan combinarse con diferentes representaciones de datos para producir una amplia variedad de software útil.
— Musser, David R.; Stepanov, Alexander A., Programación genérica [5]
El paradigma de "programación genérica" es un enfoque de descomposición de software mediante el cual los requisitos fundamentales de los tipos se abstraen de ejemplos concretos de algoritmos y estructuras de datos y se formalizan como conceptos , de manera análoga a la abstracción de teorías algebraicas en el álgebra abstracta . [6] Los primeros ejemplos de este enfoque de programación se implementaron en Scheme y Ada, [7] aunque el ejemplo más conocido es la Biblioteca de plantillas estándar (STL), [8] [9] que desarrolló una teoría de iteradores que se utiliza para desacoplar las estructuras de datos de secuencia y los algoritmos que operan en ellas.
Por ejemplo, dadas N estructuras de datos de secuencia, por ejemplo, una lista enlazada simple, un vector, etc., y M algoritmos para operar sobre ellas, por ejemplo find
, sort
etc., un enfoque directo implementaría cada algoritmo específicamente para cada estructura de datos, dando N × M combinaciones para implementar. Sin embargo, en el enfoque de programación genérica, cada estructura de datos devuelve un modelo de un concepto de iterador (un tipo de valor simple que se puede desreferenciar para recuperar el valor actual o cambiar para apuntar a otro valor en la secuencia) y cada algoritmo se escribe en cambio de forma genérica con argumentos de dichos iteradores, por ejemplo, un par de iteradores que apuntan al principio y al final de la subsecuencia o rango a procesar. Por lo tanto, solo es necesario implementar N + M combinaciones de estructura de datos-algoritmo. En la STL se especifican varios conceptos de iterador, cada uno de los cuales es un refinamiento de conceptos más restrictivos, por ejemplo, los iteradores hacia adelante solo proporcionan movimiento al siguiente valor en una secuencia (por ejemplo, adecuados para una lista enlazada simple o un flujo de datos de entrada), mientras que un iterador de acceso aleatorio también proporciona acceso directo en tiempo constante a cualquier elemento de la secuencia (por ejemplo, adecuado para un vector). Un punto importante es que una estructura de datos devolverá un modelo del concepto más general que se puede implementar de manera eficiente: los requisitos de complejidad computacional son parte explícita de la definición del concepto. Esto limita las estructuras de datos a las que se puede aplicar un algoritmo dado y dichos requisitos de complejidad son un determinante principal de la elección de la estructura de datos. La programación genérica se ha aplicado de manera similar en otros dominios, por ejemplo, algoritmos de gráficos. [10]
Aunque este enfoque a menudo utiliza características del lenguaje de la genericidad y plantillas en tiempo de compilación , es independiente de los detalles técnicos particulares del lenguaje. El pionero de la programación genérica, Alexander Stepanov, escribió:
La programación genérica trata de abstraer y clasificar algoritmos y estructuras de datos. Se inspira en Knuth y no en la teoría de tipos. Su objetivo es la construcción incremental de catálogos sistemáticos de algoritmos y estructuras de datos útiles, eficientes y abstractos. Tal empresa todavía es un sueño.
— Alexander Stepanov, Breve historia de STL [11] [12]
Creo que las teorías de iteradores son tan centrales para la informática como las teorías de anillos o espacios de Banach son centrales para las matemáticas.
— Alexander Stepanov, Entrevista con A. Stepanov [13]
Bjarne Stroustrup señaló:
Siguiendo a Stepanov, podemos definir la programación genérica sin mencionar las características del lenguaje: elevar algoritmos y estructuras de datos desde ejemplos concretos a su forma más general y abstracta.
— Bjarne Stroustrup, La evolución de un lenguaje en y para el mundo real: C++ 1991-2006 [12]
Otros paradigmas de programación que se han descrito como programación genérica incluyen la programación genérica de tipo de datos , como se describe en "Programación genérica: una introducción". [14] El enfoque de "Descartar el código repetitivo " es un enfoque de programación genérica liviano para Haskell. [15]
En este artículo distinguimos los paradigmas de programación de alto nivel de la programación genérica , mencionados anteriormente, de los mecanismos de genericidad de lenguajes de programación de nivel inferior que se utilizan para implementarlos (véase Compatibilidad de lenguajes de programación con la genericidad). Para una mayor discusión y comparación de paradigmas de programación genérica, véase. [16]
Las facilidades de genericidad han existido en lenguajes de alto nivel desde al menos la década de 1970 en lenguajes como ML , CLU y Ada , y posteriormente fueron adoptadas por muchos lenguajes basados en objetos y orientados a objetos , incluidos BETA , C++ , D , Eiffel , Java y el ahora extinto Trellis-Owl de DEC .
La genericidad se implementa y soporta de manera diferente en varios lenguajes de programación; el término "genérico" también se ha usado de manera diferente en varios contextos de programación. Por ejemplo, en Forth el compilador puede ejecutar código mientras compila y uno puede crear nuevas palabras clave del compilador y nuevas implementaciones para esas palabras sobre la marcha. Tiene pocas palabras que expongan el comportamiento del compilador y por lo tanto ofrece naturalmente capacidades de genericidad que, sin embargo, no se mencionan como tales en la mayoría de los textos de Forth. De manera similar, los lenguajes tipados dinámicamente, especialmente los interpretados, generalmente ofrecen genericidad de manera predeterminada, ya que tanto el paso de valores a funciones como la asignación de valores son indiferentes al tipo y dicho comportamiento se usa a menudo para la abstracción o la brevedad del código, sin embargo, esto no se etiqueta típicamente como genericidad ya que es una consecuencia directa del sistema de tipado dinámico empleado por el lenguaje. [ cita requerida ] El término se ha utilizado en programación funcional , específicamente en lenguajes similares a Haskell , que usan un sistema de tipos estructurales donde los tipos son siempre paramétricos y el código real en esos tipos es genérico. Estos usos aún sirven para un propósito similar de ahorro de código y renderizado de una abstracción.
Las matrices y las estructuras se pueden considerar como tipos genéricos predefinidos. Cada uso de una matriz o un tipo de estructura crea una instancia de un nuevo tipo concreto o reutiliza un tipo instanciado previamente. Los tipos de elementos de matriz y los tipos de elementos de estructura son tipos parametrizados que se utilizan para crear una instancia del tipo genérico correspondiente. Todo esto suele estar integrado en el compilador y la sintaxis difiere de otras construcciones genéricas. Algunos lenguajes de programación extensibles intentan unificar los tipos genéricos integrados y definidos por el usuario.
A continuación se presenta un estudio amplio de los mecanismos de genericidad en los lenguajes de programación. Para un estudio específico que compara la idoneidad de los mecanismos para la programación genérica, véase. [17]
Al crear clases contenedoras en lenguajes de tipado estático, resulta incómodo escribir implementaciones específicas para cada tipo de datos que contienen, especialmente si el código para cada tipo de datos es prácticamente idéntico. Por ejemplo, en C++, esta duplicación de código se puede evitar definiendo una plantilla de clase:
plantilla < typename T > clase Lista { // Contenido de la clase. }; Lista < Animal > lista_de_animales ; Lista < Coche > lista_de_coches ;
Arriba, T
hay un marcador de posición para cualquier tipo que se especifique cuando se crea la lista. Estos "contenedores de tipo T", comúnmente llamados plantillas , permiten que una clase se reutilice con diferentes tipos de datos siempre que se mantengan ciertos contratos como subtipos y firmas . Este mecanismo de genericidad no debe confundirse con el polimorfismo de inclusión , que es el uso algorítmico de subclases intercambiables: por ejemplo, una lista de objetos de tipo Moving_Object
que contiene objetos de tipo Animal
y Car
. Las plantillas también se pueden usar para funciones independientes del tipo como en el Swap
ejemplo siguiente:
// "&" denota una plantilla de referencia < typename T > void Swap ( T & a , T & b ) { // Una función similar, pero más segura y potencialmente más rápida // se define en el encabezado de la biblioteca estándar <utility> T temp = b ; b = a ; a = temp ; } std :: string mundo = "¡Mundo!" ; std :: string hola = "Hola, " ; Swap ( mundo , hola ); std :: cout << mundo << hola << '\ n ' ; // La salida es "¡Hola, mundo!".
La template
construcción de C++ utilizada anteriormente se cita ampliamente [ cita requerida ] como la construcción de genericidad que popularizó la noción entre programadores y diseñadores de lenguajes y admite muchos modismos de programación genéricos. El lenguaje de programación D también ofrece plantillas totalmente genéricas basadas en el precedente de C++ pero con una sintaxis simplificada. El lenguaje de programación Java ha proporcionado facilidades de genericidad basadas sintácticamente en C++ desde la introducción de Java Platform, Standard Edition (J2SE) 5.0.
C# 2.0, Oxygene 1.5 (anteriormente Chrome) y Visual Basic .NET 2005 tienen construcciones que explotan el soporte para genéricos presente en Microsoft .NET Framework desde la versión 2.0.
Ada ha tenido genéricos desde que se diseñó por primera vez en 1977-1980. La biblioteca estándar utiliza genéricos para proporcionar muchos servicios. Ada 2005 agrega una biblioteca de contenedores genéricos integral a la biblioteca estándar, que se inspiró en la biblioteca de plantillas estándar de C++ .
Una unidad genérica es un paquete o un subprograma que toma uno o más parámetros formales genéricos . [18]
Un parámetro formal genérico es un valor, una variable, una constante, un tipo, un subprograma o incluso una instancia de otra unidad genérica designada. Para los tipos formales genéricos, la sintaxis distingue entre tipos discretos, de punto flotante, de punto fijo, de acceso (puntero), etc. Algunos parámetros formales pueden tener valores predeterminados.
Para crear una instancia de una unidad genérica, el programador pasa parámetros reales para cada formal. La instancia genérica se comporta entonces como cualquier otra unidad. Es posible crear instancias de unidades genéricas en tiempo de ejecución , por ejemplo, dentro de un bucle.
La especificación de un paquete genérico:
genérico Max_Size : Natural ; -- un tipo de valor formal genérico Element_Type es privado ; -- un tipo formal genérico; acepta cualquier paquete de tipos no limitado Stacks es el tipo Size_Type es rango 0 .. Max_Size ; el tipo Stack es limitado privado ; procedimiento Create ( S : out Stack ; Initial_Size : in Size_Type := Max_Size ); procedimiento Push ( Into : in out Stack ; Element : in Element_Type ); procedimiento Pop ( From : in out Stack ; Element : out Element_Type ); Overflow : excepción ; Underflow : excepción ; privado subtipo Index_Type es Size_Type rango 1 .. Max_Size ; tipo Vector es una matriz ( Index_Type rango <>) de Element_Type ; tipo Stack ( Allocated_Size : Size_Type := 0 ) es registro Top : Index_Type ; Storage : Vector ( 1 .. Allocated_Size ); fin registro ; fin Stacks ;
Instanciando el paquete genérico:
tipo Bookmark_Type es nuevo Natural ; -- registra una ubicación en el documento de texto que estamos editando El paquete Bookmark_Stacks es el nuevo Stacks ( Max_Size => 20 , Element_Type => Bookmark_Type ); – Permite al usuario saltar entre ubicaciones registradas en un documento
Usando una instancia de un paquete genérico:
tipo Document_Type es registro Contenido : Ada . Strings . Unbounded . Unbounded_String ; Marcadores : Bookmark_Stacks . Stack ; fin registro ; procedimiento Editar ( Nombre_Documento : en String ) es Documento : Tipo_Documento ; comenzar -- Inicializar la pila de marcadores: Bookmark_Stacks . Crear ( S => Documento . Bookmarks , Tamaño_Inicial => 10 ); -- Ahora, abra el archivo Nombre_Documento y léalo en... fin Editar ;
La sintaxis del lenguaje permite especificar con precisión las restricciones sobre parámetros formales genéricos. Por ejemplo, es posible especificar que un tipo formal genérico solo aceptará un tipo modular como el tipo actual. También es posible expresar restricciones entre parámetros formales genéricos; por ejemplo:
el tipo genérico Index_Type es (<>); -- debe ser un tipo discreto el tipo Element_Type es privado ; -- puede ser cualquier tipo no limitado el tipo Array_Type es una matriz ( rango Index_Type <>) de Element_Type ;
En este ejemplo, Array_Type está limitado por Index_Type y Element_Type. Al crear una instancia de la unidad, el programador debe pasar un tipo de matriz real que cumpla con estas restricciones.
La desventaja de este control de grano fino es una sintaxis complicada, pero, debido a que todos los parámetros formales genéricos están completamente definidos en la especificación, el compilador puede crear instancias genéricas sin mirar el cuerpo del genérico.
A diferencia de C++, Ada no permite instancias genéricas especializadas y requiere que todos los genéricos se instancian explícitamente. Estas reglas tienen varias consecuencias:
C++ utiliza plantillas para permitir técnicas de programación genéricas. La biblioteca estándar de C++ incluye la biblioteca de plantillas estándar o STL, que proporciona un marco de plantillas para estructuras de datos y algoritmos comunes. Las plantillas en C++ también se pueden utilizar para la metaprogramación de plantillas , que es una forma de evaluar previamente parte del código en tiempo de compilación en lugar de en tiempo de ejecución . Al utilizar la especialización de plantillas, las plantillas de C++ son completas en el sentido de Turing .
Existen muchos tipos de plantillas, siendo las más comunes las plantillas de función y las plantillas de clase. Una plantilla de función es un patrón para crear funciones ordinarias basadas en los tipos de parametrización suministrados cuando se instancian. Por ejemplo, la biblioteca de plantillas estándar de C++ contiene la plantilla de función max(x, y)
que crea funciones que devuelven x o y, lo que sea mayor. max()
podría definirse de la siguiente manera:
plantilla < typename T > T max ( T x , T y ) { devolver x < y ? y : x ; }
Las especializaciones de esta plantilla de función, instancias con tipos específicos, se pueden llamar como una función ordinaria:
std :: cout << max ( 3 , 7 ); // Salida 7.
El compilador examina los argumentos utilizados para llamar max
y determina que se trata de una llamada a max(int, int)
. Luego, crea una instancia de una versión de la función donde el tipo de parametrización T
es int
, lo que genera el equivalente de la siguiente función:
int máx ( int x , int y ) { devolver x < y ? y : x ; }
Esto funciona tanto si los argumentos x
y y
son números enteros, cadenas o cualquier otro tipo para el que la expresión x < y
sea sensible, o más específicamente, para cualquier tipo para el que operator<
se defina. No se necesita herencia común para el conjunto de tipos que se pueden utilizar, por lo que es muy similar a la tipificación pato . Un programa que define un tipo de datos personalizado puede utilizar la sobrecarga de operadores para definir el significado de <
para ese tipo, lo que permite su uso con la max()
plantilla de función. Si bien esto puede parecer un beneficio menor en este ejemplo aislado, en el contexto de una biblioteca completa como STL permite al programador obtener una amplia funcionalidad para un nuevo tipo de datos, simplemente definiendo algunos operadores para él. La mera definición <
permite utilizar un tipo con los algoritmos estándar sort()
, stable_sort()
, y binary_search()
o colocarlo dentro de estructuras de datos como set
s, heaps y matrices asociativas .
Las plantillas de C++ son completamente seguras en cuanto a tipos en el momento de la compilación. Como demostración, el tipo estándar complex
no define el <
operador, porque no hay un orden estricto en los números complejos . Por lo tanto, max(x, y)
fallará con un error de compilación si x e y son complex
valores. Del mismo modo, otras plantillas que dependen de <
no se pueden aplicar a complex
los datos a menos que se proporcione una comparación (en forma de un funtor o función). Por ejemplo: A complex
no se puede usar como clave para a map
a menos que se proporcione una comparación. Desafortunadamente, históricamente los compiladores generan mensajes de error algo esotéricos, largos e inútiles para este tipo de error. Asegurarse de que un determinado objeto se adhiere a un protocolo de método puede aliviar este problema. Los lenguajes que usan compare
en lugar de <
también pueden usar complex
valores como claves.
Otro tipo de plantilla, una plantilla de clase, extiende el mismo concepto a las clases. Una especialización de plantilla de clase es una clase. Las plantillas de clase se utilizan a menudo para crear contenedores genéricos. Por ejemplo, la STL tiene un contenedor de lista enlazada . Para crear una lista enlazada de números enteros, se escribe list<int>
. Una lista de cadenas se denota como list<string>
. A list
tiene un conjunto de funciones estándar asociadas a ella, que funcionan para cualquier tipo de parametrización compatible.
Una característica poderosa de las plantillas de C++ es la especialización de plantillas . Esto permite proporcionar implementaciones alternativas basadas en ciertas características del tipo parametrizado que se está instanciando. La especialización de plantillas tiene dos propósitos: permitir ciertas formas de optimización y reducir la sobrecarga de código.
Por ejemplo, considere una sort()
función de plantilla. Una de las actividades principales que realiza dicha función es intercambiar los valores en dos de las posiciones del contenedor. Si los valores son grandes (en términos de la cantidad de bytes que se necesitan para almacenar cada uno de ellos), entonces suele ser más rápido crear primero una lista separada de punteros a los objetos, ordenar esos punteros y luego crear la secuencia ordenada final. Sin embargo, si los valores son bastante pequeños, generalmente es más rápido simplemente intercambiar los valores en el lugar según sea necesario. Además, si el tipo parametrizado ya es de algún tipo de puntero, entonces no hay necesidad de crear una matriz de punteros separada. La especialización de plantillas permite al creador de plantillas escribir diferentes implementaciones y especificar las características que deben tener los tipos parametrizados para cada implementación que se utilizará.
A diferencia de las plantillas de función, las plantillas de clase pueden estar parcialmente especializadas . Esto significa que se puede proporcionar una versión alternativa del código de la plantilla de clase cuando se conocen algunos de los parámetros de la plantilla, mientras que otros parámetros de la plantilla se dejan genéricos. Esto se puede utilizar, por ejemplo, para crear una implementación predeterminada (la especialización primaria ) que asume que copiar un tipo de parametrización es costoso y luego crear especializaciones parciales para tipos que son baratos de copiar, lo que aumenta la eficiencia general. Los clientes de una plantilla de clase de este tipo solo usan especializaciones de la misma sin necesidad de saber si el compilador usó la especialización primaria o alguna especialización parcial en cada caso. Las plantillas de clase también pueden estar completamente especializadas, lo que significa que se puede proporcionar una implementación alternativa cuando se conocen todos los tipos de parametrización.
Algunos usos de las plantillas, como la max()
función, se cubrieron anteriormente con macros de preprocesador similares a funciones (un legado del lenguaje C ). Por ejemplo, aquí se muestra una posible implementación de dicha macro:
#define máx(a,b) ((a) < (b) ? (b) : (a))
Las macros se expanden (se copian y pegan) por el preprocesador antes de compilarlas correctamente; las plantillas son funciones reales. Las macros siempre se expanden en línea; las plantillas también pueden ser funciones en línea cuando el compilador lo considere apropiado.
Sin embargo, las plantillas se consideran generalmente una mejora con respecto a las macros para estos fines. Las plantillas son seguras en cuanto a tipos. Las plantillas evitan algunos de los errores comunes que se encuentran en el código que hace un uso intensivo de macros similares a funciones, como la evaluación de parámetros con efectos secundarios dos veces. Quizás lo más importante es que las plantillas fueron diseñadas para ser aplicables a problemas mucho más grandes que las macros.
El uso de plantillas tiene cuatro inconvenientes principales: funciones compatibles, compatibilidad con el compilador, mensajes de error deficientes (normalmente con SFINAE anteriores a C++20 ) y exceso de código :
Entonces, ¿se puede utilizar la derivación para reducir el problema del código replicado debido al uso de plantillas? Esto implicaría derivar una plantilla de una clase común. Esta técnica demostró ser exitosa para frenar la hinchazón del código en el uso real. Las personas que no utilizan una técnica como esta han descubierto que el código replicado puede ocupar megabytes de espacio de código incluso en programas de tamaño moderado.
— Bjarne Stroustrup , El diseño y la evolución de C++, 1994 [20]
Las instancias adicionales generadas por las plantillas también pueden hacer que algunos depuradores tengan dificultades para trabajar correctamente con ellas. Por ejemplo, si se establece un punto de interrupción de depuración dentro de una plantilla desde un archivo de origen, es posible que no se establezca el punto de interrupción en la instancia real deseada o que se establezca un punto de interrupción en cada lugar donde se crea una instancia de la plantilla.
Además, el código fuente de implementación de la plantilla debe estar completamente disponible (por ejemplo, incluido en un encabezado) para la unidad de traducción (archivo fuente) que lo utiliza. Las plantillas, incluida gran parte de la biblioteca estándar, si no se incluyen en los archivos de encabezado, no se pueden compilar. (Esto es en contraste con el código sin plantilla, que se puede compilar en binario, proporcionando solo un archivo de encabezado de declaraciones para el código que lo usa). Esto puede ser una desventaja al exponer el código de implementación, lo que elimina algunas abstracciones y podría restringir su uso en proyectos de código cerrado. [ cita requerida ]
El lenguaje D admite plantillas basadas en diseño en C++. La mayoría de los modismos de plantillas de C++ funcionan en D sin modificaciones, pero D agrega algunas funciones:
static if
de C++ y C++ respectivamente .if constexpr
is(...)
expresión permite la instanciación especulativa para verificar las características de un objeto en tiempo de compilación.auto
palabra clave y la typeof
expresión permiten la inferencia de tipos para declaraciones de variables y valores de retorno de funciones, lo que a su vez permite "tipos Voldemort" (tipos que no tienen un nombre global). [21]Las plantillas en D utilizan una sintaxis diferente a la de C++: mientras que en C++ los parámetros de plantilla se encierran entre corchetes angulares ( Template<param1, param2>
), en D se utiliza un signo de exclamación y paréntesis: Template!(param1, param2)
. Esto evita las dificultades de análisis de C++ debido a la ambigüedad con los operadores de comparación. Si solo hay un parámetro, se pueden omitir los paréntesis.
Convencionalmente, D combina las características anteriores para proporcionar polimorfismo en tiempo de compilación mediante programación genérica basada en rasgos. Por ejemplo, un rango de entrada se define como cualquier tipo que satisfaga las comprobaciones realizadas por isInputRange
, que se define de la siguiente manera:
plantilla isInputRange ( R ) { enum bool isInputRange = is ( typeof ( ( inout int = 0 ) { R r = R . init ; // puede definir un objeto de rango if ( r . empty ) {} // puede probar si está vacío r . popFront (); // puede invocar popFront() auto h = r . front ; // puede obtener el frente del rango })); }
Una función que solo acepta rangos de entrada puede entonces usar la plantilla anterior en una restricción de plantilla:
auto fun ( Rango )( Rango rango ) si ( isInputRange ! Rango ) { // ... }
Además de la metaprogramación de plantillas, D también proporciona varias funciones para permitir la generación de código en tiempo de compilación:
import
expresión permite leer un archivo del disco y utilizar su contenido como una expresión de cadena.La combinación de lo anterior permite generar código basado en declaraciones existentes. Por ejemplo, los marcos de serialización de D pueden enumerar los miembros de un tipo y generar funciones especializadas para cada tipo serializado para realizar la serialización y deserialización. Los atributos definidos por el usuario podrían indicar además reglas de serialización.
La import
expresión y la ejecución de funciones en tiempo de compilación también permiten implementar de manera eficiente lenguajes específicos de dominio . Por ejemplo, dada una función que toma una cadena que contiene una plantilla HTML y devuelve un código fuente D equivalente, es posible usarla de la siguiente manera:
// Importa el contenido de example.htt como una constante de manifiesto de cadena. enum htmlTemplate = import ( "example.htt" ); // Transpilar la plantilla HTML a código D. enumeración htmlDCode = htmlTemplateToD ( htmlTemplate ); // Pegue el contenido de htmlDCode como código D. mixin ( htmlDCode );
Las clases genéricas han sido parte de Eiffel desde el diseño original de métodos y lenguajes. Las publicaciones fundacionales de Eiffel, [22] [23] utilizan el término genericidad para describir la creación y el uso de clases genéricas.
Las clases genéricas se declaran con su nombre de clase y una lista de uno o más parámetros genéricos formales . En el código siguiente, la clase LIST
tiene un parámetro genérico formalG
clase LISTA [ G ] ... característica -- Acceder al elemento : G -- El elemento al que apunta actualmente el cursor ... característica -- Cambio de elemento put ( new_item : G ) -- Agregar 'new_item' al final de la lista ...
Los parámetros genéricos formales son marcadores de posición para nombres de clase arbitrarios que se proporcionarán cuando se realiza una declaración de la clase genérica, como se muestra en las dos derivaciones genéricas a continuación, donde ACCOUNT
y DEPOSIT
son otros nombres de clase. ACCOUNT
y DEPOSIT
se consideran parámetros genéricos reales , ya que proporcionan nombres de clase reales para sustituir G
en el uso real.
list_of_accounts : LISTA [ CUENTA ] -- Lista de cuentas list_of_deposits : LISTA [ DEPÓSITO ] -- Lista de depósitos
Dentro del sistema de tipos de Eiffel, aunque una clase LIST [G]
se considera una clase, no se considera un tipo. Sin embargo, una derivación genérica de una clase LIST [G]
como ésta sí LIST [ACCOUNT]
se considera un tipo.
Para la clase de lista que se muestra arriba, un parámetro genérico real que sustituye a G
puede ser cualquier otra clase disponible. Para restringir el conjunto de clases de las que se pueden elegir parámetros genéricos reales válidos, se puede especificar una restricción genéricaSORTED_LIST
. En la declaración de la clase que se muestra a continuación, la restricción genérica dicta que cualquier parámetro genérico real válido será una clase que herede de la clase COMPARABLE
. La restricción genérica garantiza que los elementos de a SORTED_LIST
puedan, de hecho, ordenarse.
clase LISTA_ORDENADA [ G -> COMPARABLE ]
En 2004, se agregó soporte para los genéricos , o "contenedores de tipo T", al lenguaje de programación Java como parte de J2SE 5.0. En Java, los genéricos solo se verifican en tiempo de compilación para verificar su corrección de tipo. Luego, la información de tipo genérico se elimina mediante un proceso llamado borrado de tipo , para mantener la compatibilidad con las implementaciones antiguas de JVM , lo que la hace no disponible en tiempo de ejecución. [24] Por ejemplo, a List<String>
se convierte al tipo sin formato List
. El compilador inserta conversiones de tipo para convertir los elementos al String
tipo cuando se recuperan de la lista, lo que reduce el rendimiento en comparación con otras implementaciones como las plantillas de C++.
Los genéricos se agregaron como parte de .NET Framework 2.0 en noviembre de 2005, basándose en un prototipo de investigación de Microsoft Research iniciado en 1999. [25] Aunque son similares a los genéricos en Java, los genéricos de .NET no aplican el borrado de tipos , [26] : 208–209 pero implementan genéricos como un mecanismo de primera clase en el tiempo de ejecución utilizando la reificación . Esta opción de diseño proporciona una funcionalidad adicional, como permitir la reflexión con la preservación de los tipos genéricos y aliviar algunos de los límites del borrado (como la imposibilidad de crear matrices genéricas). [27] [28] Esto también significa que no hay impacto en el rendimiento por las conversiones en tiempo de ejecución y las conversiones boxing normalmente costosas . Cuando se usan tipos primitivos y de valor como argumentos genéricos, obtienen implementaciones especializadas, lo que permite colecciones y métodos genéricos eficientes. Al igual que en C++ y Java, los tipos genéricos anidados como Dictionary<string, List<int>> son tipos válidos, sin embargo, se desaconsejan para las firmas de miembros en las reglas de diseño de análisis de código. [29]
.NET permite seis variedades de restricciones de tipo genérico utilizando la where
palabra clave, incluida la restricción de los tipos genéricos para que sean tipos de valor, clases, tengan constructores e implementen interfaces. [30] A continuación, se muestra un ejemplo con una restricción de interfaz:
usando Sistema ; Clase de muestra { void estático principal () { int [] matriz = { 0 , 1 , 2 , 3 }; MakeAtLeast < int > ( array , 2 ); // Cambia la matriz a { 2, 2, 2, 3 } foreach ( int i en matriz ) Consola . WriteLine ( i ); // Imprimir resultados. Consola .ReadKey ( verdadero ) ; } void estático MakeAtLeast < T > ( T [] lista , T más bajo ) donde T : IComparable < T > { para ( int i = 0 ; i < lista . Longitud ; i ++ ) si ( lista [ i ]. CompareTo ( más bajo ) < 0 ) lista [ i ] = más baja ; }}
El MakeAtLeast()
método permite la operación en matrices, con elementos de tipo genérico T
. La restricción de tipo del método indica que el método es aplicable a cualquier tipo T
que implemente la IComparable<T>
interfaz genérica. Esto garantiza un error de tiempo de compilación , si se llama al método si el tipo no admite la comparación. La interfaz proporciona el método genérico CompareTo(T)
.
El método anterior también podría escribirse sin tipos genéricos, simplemente utilizando el Array
tipo no genérico. Sin embargo, dado que las matrices son contravariantes , la conversión no sería segura para los tipos y el compilador no podría encontrar ciertos posibles errores que de otro modo se detectarían al utilizar tipos genéricos. Además, el método necesitaría acceder a los elementos de la matriz como object
s en su lugar, y requeriría una conversión para comparar dos elementos. (Para tipos de valor como int
este, se requiere una conversión boxing , aunque esto se puede solucionar utilizando la Comparer<T>
clase, como se hace en las clases de colección estándar).
Un comportamiento notable de los miembros estáticos en una clase .NET genérica es la instanciación de miembros estáticos por tipo de tiempo de ejecución (ver el ejemplo a continuación).
// Una clase genérica public class GenTest < T > { // Una variable estática: se creará para cada tipo en la reflexión static CountedInstances OnePerType = new CountedInstances (); // un miembro de datos privado T _t ; // constructor simple public GenTest ( T t ) { _t = t ; } } // una clase public class CountedInstances { //Variable estática: se incrementará una vez por instancia public static int Counter ; //constructor simple public CountedInstances () { //incrementa el contador en uno durante la instanciación del objeto CountedInstances.Counter ++ ; } } // punto de entrada del código principal // al final de la ejecución, CountedInstances.Counter = 2 GenTest < int > g1 = new GenTest < int > ( 1 ); GenTest < int > g11 = new GenTest < int > ( 11 ); GenTest < int > g111 = new GenTest < int > ( 111 ); GenTest < double > g2 = new GenTest < double > ( 1.0 );
El dialecto Object Pascal de Delphi adquirió genéricos en la versión Delphi 2007, inicialmente sólo con el compilador .NET (ahora descontinuado) antes de agregarse al código nativo en la versión Delphi 2009. La semántica y las capacidades de los genéricos de Delphi se basan en gran medida en las que tenían los genéricos en .NET 2.0, aunque la implementación es necesariamente bastante diferente. A continuación se muestra una traducción más o menos directa del primer ejemplo de C# que se muestra arriba:
programa Muestra ; {$CONSOLA APPTYPE}utiliza genéricos . Valores predeterminados ; //para IComparer<> tipo TUtils = clase procedimiento de clase MakeAtLeast < T > ( Arr : TArray < T >; const Lowest : T ; Comparer : IComparer < T > ) ; sobrecarga ; procedimiento de clase MakeAtLeast < T > ( Arr : TArray < T >; const Lowest : T ) ; sobrecarga ; fin ; procedimiento de clase TUtils . MakeAtLeast < T > ( Arr : TArray < T >; const Lowest : T ; Comparer : IComparer < T > ) ; var I : Integer ; comenzar si Comparer = nil entonces Comparer := TComparer < T >. Predeterminado ; para I := Low ( Arr ) a High ( Arr ) hacer si Comparer . Compare ( Arr [ I ] , Lowest ) < 0 entonces Arr [ I ] := Lowest ; fin ; procedimiento de clase TUtils . MakeAtLeast < T > ( Arr : TArray < T >; const Lowest : T ) ; inicio MakeAtLeast < T > ( Arr , Lowest , nil ) ; fin ; var Ints : TArray < Integer >; Valor : Integer ; inicio Ints := TArray < Integer >. Create ( 0 , 1 , 2 , 3 ) ; TUtils . MakeAtLeast < Integer > ( Ints , 2 ) ; para Valor en Ints hacer WriteLn ( Valor ) ; ReadLn ; fin .
Al igual que en C#, los métodos y los tipos completos pueden tener uno o más parámetros de tipo. En el ejemplo, TArray es un tipo genérico (definido por el lenguaje) y MakeAtLeast es un método genérico. Las restricciones disponibles son muy similares a las restricciones disponibles en C#: cualquier tipo de valor, cualquier clase, una clase o interfaz específica y una clase con un constructor sin parámetros. Las restricciones múltiples actúan como una unión aditiva.
Free Pascal implementó los genéricos antes que Delphi, y con una sintaxis y una semántica diferentes. Sin embargo, desde la versión 2.6.0 de FPC, la sintaxis de estilo Delphi está disponible cuando se utiliza el modo de lenguaje {$mode Delphi}. Por lo tanto, el código de Free Pascal admite genéricos en ambos estilos.
Ejemplo de Delphi y Free Pascal:
// Unidad estilo Delphi A ; {$ifdef fpc} {$modo delphi} {$endif} interfaztipo TGenericClass < T > = clase función Foo ( const AValue : T ) : T ; fin ; implementaciónfunción TGenericClass < T >. Foo ( const AValor : T ) : T ; comenzar resultado : = AValue + AValue ; fin ; fin .// Unidad B de estilo ObjFPC de Free Pascal ; {$ifdef fpc} {$modo objfpc} {$endif} interfaztipo genérico TGenericClass < T > = clase función Foo ( const AValue : T ) : T ; fin ; implementaciónfunción TGenericClass . Foo ( const AValor : T ) : T ; comenzar resultado : = AValor + AValor ; fin ; fin .// ejemplo de uso, programa estilo Delphi TestGenDelphi ; {$ifdef fpc} {$modo delphi} {$endif} utiliza A , B ; var GC1 : A . TGenericClass < Entero >; GC2 : B . TGenericClass < Cadena >; comienzo GC1 := A . TGenericClass < Entero >. Crear ; GC2 := B . TGenericClass < Cadena >. Crear ; WriteLn ( GC1 . Foo ( 100 )) ; // 200 WriteLn ( GC2 . Foo ( 'hola' )) ; // holahola GC1 . Libre ; GC2 . Libre ; fin . // ejemplo de uso, programa estilo ObjFPC TestGenDelphi ; {$ifdef fpc} {$modo objfpc} {$endif} utiliza A , B ; // requerido en el tipo ObjFPC TAGenericClassInt = specialist A . TGenericClass < Integer >; TBGenericClassString = specialist B . TGenericClass < String >; var GC1 : TAGenericClassInt ; GC2 : TBGenericClassString ; begin GC1 := TAGenericClassInt . Create ; GC2 := TBGenericClassString . Create ; WriteLn ( GC1 . Foo ( 100 )) ; // 200 WriteLn ( GC2 . Foo ( 'hola' )) ; // holahola GC1 . Free ; GC2 . Free ; end .
El mecanismo de clases de tipos de Haskell admite la programación genérica. Seis de las clases de tipos predefinidas en Haskell (incluidos Eq
, los tipos que se pueden comparar para determinar su igualdad, y Show
, los tipos cuyos valores se pueden representar como cadenas) tienen la propiedad especial de admitir instancias derivadas. Esto significa que un programador que defina un nuevo tipo puede indicar que este tipo será una instancia de una de estas clases de tipos especiales, sin proporcionar implementaciones de los métodos de clase como suele ser necesario al declarar instancias de clase. Todos los métodos necesarios se "derivarán", es decir, se construirán automáticamente, en función de la estructura del tipo. Por ejemplo, la siguiente declaración de un tipo de árboles binarios indica que será una instancia de las clases Eq
y Show
:
datos BinTree a = Hoja a | Nodo ( BinTree a ) a ( BinTree a ) derivando ( Eq , Mostrar )
Esto da como resultado que se definan automáticamente una función de igualdad ( ==
) y una función de representación de cadena ( show
) para cualquier tipo de formulario, BinTree T
siempre que T
éste admita esas operaciones.
El soporte para instancias derivadas de Eq
y Show
hace que sus métodos ==
y show
sean genéricos de una manera cualitativamente diferente a las funciones paramétricamente polimórficas: estas "funciones" (más precisamente, familias de funciones indexadas por tipo) se pueden aplicar a valores de varios tipos, y aunque se comportan de manera diferente para cada tipo de argumento, se necesita poco trabajo para agregar soporte para un nuevo tipo. Ralf Hinze (2004) ha demostrado que se puede lograr un efecto similar para clases de tipos definidas por el usuario mediante ciertas técnicas de programación. Otros investigadores han propuesto enfoques para este y otros tipos de genericidad en el contexto de Haskell y extensiones a Haskell (discutidas más adelante).
PolyP fue la primera extensión genérica del lenguaje de programación Haskell . En PolyP, las funciones genéricas se denominan politípicas . El lenguaje introduce una construcción especial en la que dichas funciones politípicas se pueden definir mediante inducción estructural sobre la estructura del funtor de patrón de un tipo de datos regular. Los tipos de datos regulares en PolyP son un subconjunto de los tipos de datos Haskell. Un tipo de datos regular t debe ser de tipo * → * , y si a es el argumento de tipo formal en la definición, entonces todas las llamadas recursivas a t deben tener la forma ta . Estas restricciones descartan los tipos de datos de tipo superior y los tipos de datos anidados, donde las llamadas recursivas son de una forma diferente. La función flatten en PolyP se proporciona aquí como ejemplo:
aplanar :: Regular d => d a -> [ a ] aplanar = cata fl politípico fl :: f a [ a ] -> [ a ] caso f de g + h -> o bien fl fl g * h -> \ ( x , y ) -> fl x ++ fl y () -> \ x -> [] Par -> \ x -> [ x ] Rec -> \ x -> x d @ g -> concat . flatten . pmap fl Con t -> \ x -> [] cata :: Regular d => ( FunctorOf d a b -> b ) -> d a -> b
Generic Haskell es otra extensión de Haskell , desarrollada en la Universidad de Utrecht en los Países Bajos . Las extensiones que proporciona son:
El valor indexado por tipo resultante se puede especializar para cualquier tipo.
Como ejemplo, la función de igualdad en Haskell genérico: [31]
tipo Eq {[ * ]} t1 t2 = t1 -> t2 -> Bool tipo Eq {[ k -> l ]} t1 t2 = para todo u1 u2 . Eq {[ k ]} u1 u2 -> Eq {[ l ]} ( t1 u1 ) ( t2 u2 ) ecuación { | t :: k | } :: Ecuación {[ k ]} t t eq { | Unidad | } _ _ = Verdadero eq { | :+: | } eqA eqB ( Inl a1 ) ( Inl a2 ) = eqA a1 a2 eq { | :+: | } eqA eqB ( Inr b1 ) ( Inr b2 ) = eqB b1 b2 eq { | :+: | } eqA eqB _ _ = Falso eq { | :*: | } eqA eqB ( a1 :*: b1 ) ( a2 :*: b2 ) = eqA a1 a2 && eqB b1 b2 eq { | internacional | } = ( == ) ecuación { | Carbón | } = ( == ) ecuación { | booleano | } = ( == )
Clean ofrece programación genérica basada en PolyP y Haskell genérico, tal como lo admite GHC ≥ 6.0. Parametriza por tipo, pero ofrece sobrecarga.
Los lenguajes de la familia ML admiten la programación genérica a través del polimorfismo paramétrico y módulos genéricos llamados funtores. Tanto Standard ML como OCaml proporcionan funtores, que son similares a las plantillas de clase y a los paquetes genéricos de Ada. Las abstracciones sintácticas de Scheme también tienen una conexión con la genericidad: de hecho, son un superconjunto de plantillas de C++.
Un módulo Verilog puede tomar uno o más parámetros, a los cuales se les asignan sus valores reales al crear la instancia del módulo. Un ejemplo es una matriz de registros genéricos donde el ancho de la matriz se proporciona mediante un parámetro. Una matriz de este tipo, combinada con un vector de cable genérico, puede generar un módulo de memoria o búfer genérico con un ancho de bits arbitrario a partir de la implementación de un solo módulo. [32]
VHDL , al derivarse de Ada, también tiene capacidades genéricas. [33]
C admite "expresiones genéricas de tipo" utilizando la _Generic
palabra clave: [34]
#define cbrt(x) _Generic((x), largo doble: cbrtl, \ predeterminado: cbrt, \ flotante: cbrtf)(x)