Programación genérica

format_list_bulleted Contenido keyboard_arrow_down
ImprimirCitar
Estilo de programación informática

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 que se especificarán más tarde que luego se instancian. i> cuando sea necesario para tipos específicos proporcionados como parámetros. Este enfoque, iniciado por el lenguaje de programación ML en 1973, permite escribir funciones o tipos comunes que difieren solo en el conjunto de tipos en los que operan cuando se usan, lo que reduce la duplicación. 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 comunidad de Haskell también usa el término "genérico" para un concepto relacionado pero algo diferente); plantillas en C++ y D; y tipos parametrizados en el influyente libro de 1994 Design Patterns.

El término "programación genérica" fue acuñado originalmente por David Musser y Alexander Stepanov en un sentido más específico que el anterior, para describir un paradigma de programación 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, con funciones genéricas implementadas en términos de estos conceptos, normalmente utilizando mecanismos de generidad del lenguaje como se describe anteriormente.

Stepanov–Musser y otros paradigmas de programación genéricos

La programación genérica se define en Musser & Stepanov (1989) de la siguiente manera,

Centros de programación genéricos alrededor de la idea de abstracción de algoritmos concretos y eficientes para obtener algoritmos genéricos que pueden combinarse con diferentes representaciones de datos para producir una amplia variedad de software útil.

Musser, David R.; Stepanov, Alexander A., Generic Programming

La "programación genérica" El paradigma es un enfoque de la descomposición del 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 forma análoga a la abstracción de las teorías algebraicas en el álgebra abstracta. Los primeros ejemplos de este enfoque de programación se implementaron en Scheme y Ada, aunque el ejemplo más conocido es la Biblioteca de plantillas estándar (STL), 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, p. lista enlazada individualmente, vector, etc., y algoritmos M para operar sobre ellos, p. find, sort etc., un enfoque directo implementaría cada algoritmo específicamente para cada estructura de datos, dando N × M combinaciones a 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 su lugar. genéricamente con argumentos de tales iteradores, p. 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 combinaciones de estructura de datos-algoritmo N + M. En la STL se especifican varios conceptos de iterador, cada uno de los cuales es un refinamiento de conceptos más restrictivos, p. Los iteradores de avance solo brindan movimiento al siguiente valor en una secuencia (por ejemplo, adecuado para una lista enlazada individualmente 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 tales requisitos de complejidad son un determinante importante de la elección de la estructura de datos. La programación genérica se ha aplicado de manera similar en otros dominios, p. algoritmos de grafos.

Tenga en cuenta que aunque este enfoque a menudo utiliza características de lenguaje de genericidad/plantillas en tiempo de compilación, de hecho es independiente de los detalles técnicos del lenguaje en particular. El pionero de la programación genérica, Alexander Stepanov, escribió:

La programación genérica consiste en abstraer y clasificar algoritmos y estructuras de datos. Se inspira en Knuth y no en la teoría del tipo. Su objetivo es la construcción gradual de catálogos sistemáticos de algoritmos útiles, eficientes y abstractos y estructuras de datos. Ese compromiso sigue siendo un sueño.

Alexander Stepanov, Short History of STL

Creo que las teorías del iterador son tan centrales para la Ciencia de la Computación como teorías de anillos o espacios de Banach son centrales para las matemáticas.

Alexander Stepanov, Entrevista con A. Stepanov

Bjarne Stroustrup señaló:

Siguiendo Stepanov, podemos definir la programación genérica sin mencionar las características del lenguaje: Levantar algoritmos y estructuras de datos de ejemplos concretos a su forma más general y abstracta.

Bjarne Stroustrup, Evolución de un lenguaje en y para el mundo real: C++ 1991-2006

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". El enfoque Deseche su repetitivo es un enfoque de programación genérico ligero para Haskell.

En este artículo distinguimos los paradigmas de programación de alto nivel de programación genérica, arriba, de los mecanismos de genericidad del lenguaje de programación de nivel inferior que se usan para implementarlos (ver Programación soporte lingüístico para la genericidad). Para mayor discusión y comparación de paradigmas de programación genéricos, consulte.

Soporte de lenguaje de programación para genericidad

Las funciones 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 orientados a objetos y basados en objetos, incluidos BETA, C++, D, Eiffel., Java y el lenguaje Trellis-Owl ahora desaparecido de DEC.

Genericity se implementa y admite de manera diferente en varios lenguajes de programación; el término "genérico" también se ha utilizado 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 tipificados dinámicamente, especialmente los interpretados, generalmente ofrecen genericidad de forma predeterminada, ya que tanto el paso de valores a las funciones como la asignación de valores son indiferentes al tipo y dicho comportamiento a menudo se utiliza para la abstracción o la concisión del código, sin embargo, esto es no suele etiquetarse como genericidad ya que es una consecuencia directa del sistema de tipeo dinámico empleado por el idioma. El término se ha utilizado en la programación funcional, específicamente en lenguajes similares a Haskell, que utilizan un sistema de tipo estructural donde los tipos son siempre paramétricos y el código real de esos tipos es genérico. Estos usos aún tienen un propósito similar de guardar código y representar una abstracción.

Las matrices y las estructuras se pueden ver como tipos genéricos predefinidos. Cada uso de un tipo de matriz o estructura crea una instancia de un nuevo tipo concreto o reutiliza un tipo instanciado anterior. Los tipos de elementos de matriz y los tipos de elementos de estructura son tipos parametrizados, que se utilizan para crear instancias 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 amplio estudio de los mecanismos de genericidad en los lenguajes de programación. Para una encuesta específica que compara la idoneidad de los mecanismos para la programación genérica, ver.

En lenguajes orientados a objetos

Al crear clases contenedoras en lenguajes tipificados estáticamente, es un inconveniente escribir implementaciones específicas para cada tipo de datos contenido, 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.nombre Tclase Lista {} // Contenido de clase.};Lista.Animal list_of_animals;Lista.Car list_of_cars;

Arriba, T es 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 reutilizar una clase con diferentes tipos de datos siempre que se mantengan ciertos contratos, como los subtipos y la firma. 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 Coche. Las plantillas también se pueden usar para funciones independientes del tipo, como en el siguiente ejemplo de Swap:

// " tardía" denota una referenciaplantilla.nombre Tvacío 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 biblioteca estándar T tempestad = b; b = a; a = tempestad;}std::cuerda mundo = "¡Mundo!";std::cuerda Hola. = "Hola".;Swap()mundo, Hola.);std::Cout .. mundo .. Hola. .. 'n; // La salida es "Hola, Mundo!".

La construcción template de C++ utilizada anteriormente es ampliamente citada como la construcción de genericidad que popularizó la noción entre programadores y diseñadores de lenguajes y es compatible con muchos lenguajes 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 funciones de genericidad sintácticamente basadas en C++ desde la introducción de J2SE 5.0.

C# 2.0, Oxygene 1.5 (también conocido como Chrome) y Visual Basic.NET 2005 tienen construcciones que aprovechan la compatibilidad con genéricos presente en Microsoft.NET Framework desde la versión 2.0.

Genéricos en Ada

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 completa biblioteca de contenedores genéricos 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.

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 instanciar una unidad genérica, el programador pasa parámetros actuales para cada formal. La instancia genérica se comporta como cualquier otra unidad. Es posible crear instancias de unidades genéricas en tiempo de ejecución, por ejemplo, dentro de un bucle.

Ejemplo

La especificación de un paquete genérico:

 genérico genérico Max_Size : Natural; - un valor formal genérico Tipo Element_Type es privado; -- un tipo formal genérico; acepta cualquier tipo no limitado paquete Stacks es Tipo Size_Type es rango 0 .. Max_Size; Tipo Stack es limitada privado; procedimiento Crear ()S : Fuera. Stack; Initial_Size : dentro Size_Type := Max_Size); procedimiento Empuja ()Into : dentro Fuera. Stack; Elemento : dentro Element_Type); procedimiento Pop ()Desde : dentro Fuera. Stack; Elemento : Fuera. Element_Type); Desbordamiento : excepción; Corrientes : excepción; privado subtipo Index_Type es Size_Type rango 1 .. Max_Size; Tipo Vectorial es array ()Index_Type rango  de Element_Type; Tipo Stack ()Allocated_Size : Size_Type := 0) es récord Top : Index_Type; Almacenamiento : Vectorial ()1 .. Allocated_Size); final record; final Stacks;

Instanciación del paquete genérico:

 Tipo Bookmark_ Tipo es nuevo Natural; -- registra un lugar en el documento de texto que estamos editando paquete Bookmark_Stacks es nuevo Stacks ()Max_Size = 20, Element_Type = Bookmark_ Tipo); -- Permite al usuario saltar entre ubicaciones registradas en un documento

Usando una instancia de un paquete genérico:

 Tipo Documento_ Tipo es récord Índice : Ada.Pendientes.Sin límites.Unbounded_String; Marcas : Bookmark_Stacks.Stack; final record; procedimiento Editar ()Document_Name : dentro String) es Documento : Documento_ Tipo; comenzar -- Inicia la pila de marcadores: Bookmark_Stacks.Crear ()S = Documento.Marcas, Initial_Size = 10); -- Ahora, abre el archivo Document_Name y lee en... final Editar;
Ventajas y limitaciones

La sintaxis del lenguaje permite la especificación precisa de restricciones en parámetros formales genéricos. Por ejemplo, es posible especificar que un tipo formal genérico solo aceptará un tipo modular como real. También es posible expresar restricciones entre parámetros formales genéricos; Por ejemplo:

 genérico genérico Tipo Index_Type es (Seguido); - debe ser un tipo discreto Tipo Element_Type es privado; -- puede ser cualquier tipo no limitado Tipo Array_Type es array ()Index_Type rango  de Element_Type;

En este ejemplo, Array_Type está restringido tanto por Index_Type como por Element_Type. Al instanciar la unidad, el programador debe pasar un tipo de matriz real que satisfaga estas restricciones.

La desventaja de este control detallado 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 de genéricos 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:

  • el compilador puede implementar genéricos compartidos: el código objeto de una unidad genérica se puede compartir entre todos los casos (a menos que las solicitudes de programador indiquen subprogramas, por supuesto). As further consequences:
    • no hay posibilidad de bloat de código (la bloat de código es común en C++ y requiere atención especial, como se explica a continuación).
    • es posible instantánear los genéricos en tiempo de ejecución, así como en tiempo de compilación, ya que no se requiere ningún nuevo código de objeto para una nueva instancia.
    • Los objetos reales correspondientes a un objeto formal genérico se consideran siempre no estáticos dentro del genérico; véase objetos formales genéricos en el Wikibook para detalles y consecuencias.
  • todas las instancias de un ser genérico exactamente igual, es más fácil revisar y entender los programas escritos por otros; no hay "casos especiales" que tener en cuenta.
  • todas las instantáneas siendo explícitas, no hay instantáneas ocultas que podrían dificultar la comprensión del programa.
  • Ada no permite "template metaprogramming", porque no permite especializaciones.

Plantillas en C++

C++ usa 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 usar 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. Usando la especialización de plantillas, las plantillas de C++ se consideran completas de Turing.

Resumen técnico

Hay muchos tipos de plantillas, las más comunes son 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 proporcionados 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, cualquiera que sea es más grande max() podría definirse así:

plantilla.nombre TT max()T x, T Sí.) {} retorno x . Sí. ? Sí. : x;}

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); // Productos 7.

El compilador examina los argumentos usados para llamar a max y determina que se trata de una llamada a max(int, int). Luego instancia una versión de la función donde el tipo de parametrización T es int, haciendo el equivalente de la siguiente función:

int max()int x, int Sí.) {} retorno x . Sí. ? Sí. : x;}

Esto funciona tanto si los argumentos x como y son enteros, cadenas o cualquier otro tipo para el que la expresión x < y es sensato, o más específicamente, para cualquier tipo para el que se defina operador<. La herencia común no es necesaria para el conjunto de tipos que se pueden usar, por lo que es muy similar a la tipificación pato. Un programa que define un tipo de datos personalizado puede usar la sobrecarga de operadores para definir el significado de < para ese tipo, lo que permite su uso con la plantilla de función max(). Si bien esto puede parecer un beneficio menor en este ejemplo aislado, en el contexto de una biblioteca integral 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 de < permite utilizar un tipo con el estándar sort(), stable_sort() y binary_search() o para colocarse dentro de estructuras de datos como sets, heaps y matrices asociativas.

Las plantillas de C++ son completamente seguras 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 valores complex. Del mismo modo, otras plantillas que dependen de < no se pueden aplicar a datos complex a menos que se proporcione una comparación (en forma de funtor o función). Por ejemplo: un complejo no se puede usar como clave para un mapa a menos que se proporcione una comparación. Desafortunadamente, históricamente los compiladores generan mensajes de error un tanto esotéricos, largos e inútiles para este tipo de error. Asegurarse de que un determinado objeto se adhiera a un protocolo de método puede aliviar este problema. Los idiomas que usan compare en lugar de < también pueden usar valores complex 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 hacer una lista enlazada de enteros, se escribe list<int>. Una lista de cadenas se denomina list<string>. Una list tiene un conjunto de funciones estándar asociadas, que funcionan para cualquier tipo de parametrización compatible.

Especialización de plantilla

Una potente función 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 función de plantilla sort(). Una de las actividades principales que realiza dicha función es intercambiar o cambiar los valores en dos de las posiciones del contenedor. Si los valores son grandes (en términos de la cantidad de bytes necesarios para almacenar cada uno de ellos), a menudo es más rápido construir primero una lista separada de punteros a los objetos, ordenar esos punteros y luego construir la secuencia ordenada final.. Sin embargo, si los valores son bastante pequeños, generalmente es más rápido 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 construir una matriz de puntero 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 funciones, las plantillas de clases pueden especializarse parcialmente. Eso 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, dejando otros parámetros genéricos. Esto se puede usar, 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, aumentando así la eficiencia. 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 principal o alguna especialización parcial en cada caso. Las plantillas de clase también pueden ser totalmente especializadas, lo que significa que se puede proporcionar una implementación alternativa cuando se conocen todos los tipos de parametrización.

Ventajas y desventajas

Algunos usos de las plantillas, como la función max(), anteriormente se completaban con macros de preprocesador similares a funciones (un legado del lenguaje de programación C). Por ejemplo, aquí hay una posible implementación de dicha macro:

#define max(a,b) ((a) se realizó (b) ? (b): (a))

Las macros se expanden (copian y pegan) mediante el preprocesador, antes de la compilación propiamente dicha; las plantillas son funciones reales 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 generalmente se consideran una mejora sobre las macros para estos fines. Las plantillas son de tipo seguro. 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 evaluar parámetros con efectos secundarios dos veces. Quizás lo más importante es que las plantillas se diseñaron para ser aplicables a problemas mucho más grandes que las macros.

Hay cuatro inconvenientes principales en el uso de plantillas: funciones admitidas, compatibilidad con el compilador, mensajes de error deficientes (normalmente con SFINAE anterior a C++20) y exceso de código:

  1. Las plantillas en C++ carecen de muchas características, lo que hace implementarlas y utilizarlas de manera sencilla a menudo imposible. En su lugar, los programadores tienen que confiar en trucos complicados que conduce a hincharse, difícil de entender y difícil de mantener código. Los desarrollos actuales en las normas C++ exacerban este problema haciendo un uso pesado de estos trucos y construyendo muchas nuevas características para plantillas en ellos o con ellos en mente.
  2. Muchos compiladores históricamente tenían poco apoyo para plantillas, por lo que el uso de plantillas podría haber hecho un código algo menos portátil. El soporte también puede ser pobre cuando se utiliza un compilador C++ con un linker que no es C+++-aware, o cuando intenta utilizar plantillas a través de límites compartidos de biblioteca.
  3. Los compiladores pueden producir mensajes de error confusos, largos y a veces poco útiles cuando se detectan errores en código que utiliza SFINAE. Esto puede hacer que las plantillas sean difíciles de desarrollar.
  4. Finalmente, el uso de plantillas requiere que el compilador genere un separado ejemplo de la clase o función de plantilla para cada permutación de parámetros de tipo utilizados con ella. (Esto es necesario porque los tipos en C+ no son del mismo tamaño, y los tamaños de los campos de datos son importantes para cómo funcionan las clases). Así que el uso indiscriminado de plantillas puede llevar a la fosa de código, dando lugar a ejecuciones excesivamente grandes. Sin embargo, el uso juicioso de la especialización y derivación de la plantilla puede reducir drásticamente tal bloque de código en algunos casos:

Entonces, ¿puede utilizarse la derivación para reducir el problema del código replicado porque se utilizan plantillas? Esto implicaría la obtención de una plantilla de una clase ordinaria. Esta técnica resultó exitosa en la malla de código en uso real. Las personas que no utilizan una técnica como esta han encontrado que el código replicado puede costar megabytes de espacio de código incluso en programas de tamaño moderado.

Bjarne Stroustrup, The Design and Evolution of C++, 1994

Las instancias adicionales generadas por las plantillas también pueden causar que algunos depuradores tengan dificultades para trabajar correctamente con las plantillas. Por ejemplo, establecer un punto de interrupción de depuración dentro de una plantilla desde un archivo de origen puede omitir establecer el punto de interrupción en la creación de instancias real deseada o puede establecer 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 contrasta 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.

Plantillas en D

El lenguaje de programación D admite plantillas basadas en diseño en C++. La mayoría de los modismos de plantilla de C++ se trasladarán a D sin alteración, pero D agrega algunas funciones adicionales:

  • Los parámetros de plantilla en D no se limitan a tipos y valores primitivos (como lo fue en C++ antes de C++20), sino que también permiten valores arbitrarios de tiempo de compilación (como cadenas y struct literales), y alias a identificadores arbitrarios, incluyendo otras plantillas o instantáneas de plantilla.
  • Limitaciones de la plantilla y static if declaración proporciona una alternativa a los conceptos y conceptos C+++ respectivamente if constexpr.
  • El is(...) expresión permite la instantánea especulativa para verificar los rasgos de un objeto en el tiempo de compilación.
  • El auto palabra clave y la typeof expresión permite inferencia tipo para declaraciones variables y valores de rendimiento de función, que a su vez permite "tipos de voldemort" (tipos que no tienen un nombre global).

Las plantillas en D usan una sintaxis diferente a la de C++: mientras que en C++ los parámetros de plantilla están entre corchetes angulares (Template<param1, param2>), D usa 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 funciones anteriores para proporcionar polimorfismo en tiempo de compilación utilizando programación genérica basada en características. 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 = es()tipo de() ()fuera int = 0) {} R r = R.init; // puede definir un objeto de rango si ()r.vacío) {} // puede probar para vacío r.popFront(); // puede invocar popFront() auto h = r.frontal; // puede conseguir el frente de la gama }));}

Una función que acepta solo rangos de entrada puede usar la plantilla anterior en una restricción de plantilla:

auto diversión()Rango)Rango rango) si ()isInputRange!Rango){} //...}
Generación de código

Además de la metaprogramación de plantillas, D también proporciona varias funciones para habilitar la generación de código en tiempo de compilación:

  • El import expresión permite leer un archivo del disco y utilizar su contenido como expresión de cadena.
  • La reflexión compilada permite enumerar e inspeccionar las declaraciones y sus miembros durante la compilación.
  • Los atributos definidos por el usuario permiten a los usuarios adjuntar identificadores arbitrarios a las declaraciones, que luego se pueden enumerar mediante la reflexión compilada.
  • La ejecución de funciones de tiempo compilado (CTFE) permite que un subconjunto de D (restricted to safe operations) sea interpretado durante la compilación.
  • Las mezclas de cuerda permiten evaluar y compilar el contenido de una expresión de cadena como código D que se convierte en parte del programa.

La combinación de lo anterior permite generar código basado en declaraciones existentes. Por ejemplo, los marcos de trabajo de serialización 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 las reglas de serialización.

La expresión import y la ejecución de la función 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 el código fuente D equivalente, es posible usarla de la siguiente manera:

// Importar el contenido de ejemplo.htt como una cadena manifiesta constante.enum htmlTemplate = importación()"example.htt");// Transpile la plantilla HTML al código D.enum htmlDCode = htmlTemplateToD()htmlTemplate);// Pruebe el contenido de htmlDCode como código D.mixin()htmlDCode);

Genericidad en Eiffel

Las clases genéricas han sido parte de Eiffel desde el método original y el diseño del lenguaje. Las publicaciones de la fundación de Eiffel utilizan el término genericidad para describir la creación y el uso de clases genéricas.

Genericidad básica/sin restricciones

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 siguiente código, la clase LIST tiene un parámetro genérico formal G

clase LISTA [G] ...función - Acceso Tema: G -- El tema se señala actualmente por cursor ...función - Cambio de elemento # ()new_item: G) -- Add `new_item' at the end of the list ...

Los parámetros genéricos formales son marcadores de posición para nombres de clase arbitrarios que se proporcionarán cuando se haga una declaración de la clase genérica, como se muestra en las dos derivaciones genéricas a continuación, donde CUENTA y DEPÓSITO son otros nombres de clase. CUENTA y DEPÓSITO se consideran parámetros genéricos reales ya que proporcionan nombres de clases reales para sustituir a G en uso real.

 list_of_accounts: LISTA [ACCOUNT] -- Lista de cuentas list_of_deposits: LISTA [DEPOSIT] -- Lista de depósitos

Dentro del sistema de tipos de Eiffel, aunque la clase LIST [G] se considera una clase, no se considera un tipo. Sin embargo, una derivación genérica de LIST [G] como LIST [ACCOUNT] se considera un tipo.

Genericidad restringida

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érica. En la declaración de la clase SORTED_LIST 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 una SORTED_LIST puedan ordenarse.

clase SORTED_LIST [G - COMPARABLE]

Genéricos en Java

Soporte para los genéricos, o "contenedores-de-tipo-T" se agregó al lenguaje de programación Java en 2004 como parte de J2SE 5.0. En Java, los genéricos solo se verifican en el momento de la compilación para verificar la corrección del tipo. Luego, la información de tipo genérico se elimina a través de un proceso llamado borrado de tipo, para mantener la compatibilidad con las implementaciones de JVM antiguas, lo que hace que no esté disponible en tiempo de ejecución. Por ejemplo, una List<String> se convierte al tipo sin formato List. El compilador inserta conversiones de tipo para convertir los elementos al tipo String cuando se recuperan de la lista, lo que reduce el rendimiento en comparación con otras implementaciones, como las plantillas de C++.

Genericidad en.NET [C#, VB.NET]

Los genéricos se agregaron como parte de.NET Framework 2.0 en noviembre de 2005, según un prototipo de investigación de Microsoft Research iniciado en 1999. Aunque son similares a los genéricos en Java, los genéricos de.NET no aplican el borrado de tipos, sino que los implementan como un mecanismo de primera clase en el tiempo de ejecución utilizando la cosificación. Esta elección de diseño proporciona una funcionalidad adicional, como permitir la reflexión con la conservación de tipos genéricos, así como aliviar algunas de las limitaciones del borrado (como la imposibilidad de crear matrices genéricas). Esto también significa que no hay impacto en el rendimiento de los lanzamientos en tiempo de ejecución y las conversiones de boxeo normalmente costosas. Cuando los tipos primitivos y de valor se usan 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.

.NET permite seis variedades de restricciones de tipo genérico utilizando la palabra clave where, incluida la restricción de tipos genéricos para que sean tipos de valor, clases, constructores e implementación de interfaces. A continuación se muestra un ejemplo con una restricción de interfaz:

utilizando Sistema;clase Muestra{} estática vacío Main() {} int[] array = {} 0, 1, 2, 3 }; MakeAtLeast.int.array, 2); // Cambiar la matriz a { 2, 2, 2, 3 } en adelante ()int i dentro array) Consola.WriteLine()i); // Resultados de impresión. Consola.Leer Clave()verdadero); } estática vacío MakeAtLeast.T.T[] lista, T más bajo) Donde T : IComparable.T {} para ()int i = 0; i . lista.Duración; i++) si ()lista[i].CompareTo()más bajo) . 0) lista[i] = más bajo; }}

El método MakeAtLeast() permite operar sobre arreglos, 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 interfaz genérica IComparable<T>. Esto asegura 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 usando el tipo no genérico Array. Sin embargo, dado que las matrices son contravariantes, la conversión no sería segura y el compilador no podría encontrar ciertos errores posibles que, de lo contrario, se detectarían al usar tipos genéricos. Además, el método necesitaría acceder a los elementos de la matriz como objeto y requeriría una conversión para comparar dos elementos. (Para tipos de valor como tipos como int, esto requiere una conversión boxing, aunque esto se puede solucionar usando la clase Comparer<T>, como se hace en la colección estándar clases.)

Un comportamiento notable de los miembros estáticos en una clase genérica.NET es la creación de instancias de miembros estáticos por tipo de tiempo de ejecución (consulte el ejemplo a continuación).

 / Una clase genérica público clase GenTest.T {} //Una variable estática - se creará para cada tipo de reflexión estática Cuentadas OnePerType = nuevo Cuentadas(); //un miembro de datos privado T mT; //simple constructor público GenTest()T pT) {} mT = pT; } } //a class público clase Cuentadas {} //Variabilidad estadística - esto se incrementará una vez por ejemplo público estática int Contrato; //simple constructor público Cuentadas() {} // aumentar el contador por uno durante la instantánea del objeto Cuentadas.Contrato++; } } // punto de entrada de código principal // al final de la ejecución, CondeadoInstances. Contador = 2 GenTest.int g1 = nuevo GenTest.int.1); GenTest.int g11 = nuevo GenTest.int.11); GenTest.int g111 = nuevo GenTest.int.111); GenTest.doble g2 = nuevo GenTest.doble.1.0);

Genericidad en Delphi

El dialecto Object Pascal de Delphi adquirió genéricos en la versión Delphi 2007, inicialmente solo 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 tienen los genéricos en.NET 2.0, aunque la implementación es, por necesidad, bastante diferente. Aquí hay una traducción más o menos directa del primer ejemplo de C# que se muestra arriba:

programa Muestra;{$APPTYPE CONSOLE}uso Genéricos.Defaults; //para el IComparadorTipo TUtils = clase clase procedimiento MakeAtLeast.T()Arr: TARE.T" const Más bajo: T; Comparer: IComparer.T); sobrecarga; clase procedimiento MakeAtLeast.T()Arr: TARE.T" const Más bajo: T); sobrecarga; final;clase procedimiento TUtils.MakeAtLeast.T()Arr: TARE.T" const Más bajo: T; Comparer: IComparer.T);Var I: Integer;comenzar si Comparer = Nil entonces Comparer := TComparer.T.Default; para I := Baja()Arr) a Alto()Arr) do si Comparer.Compare()Arr[I], Más bajo) . 0 entonces Arr[I] := Más bajo;final;clase procedimiento TUtils.MakeAtLeast.T()Arr: TARE.T" const Más bajo: T);comenzar MakeAtLeast.T()Arr, Más bajo, Nil);final;Var Ints: TARE.Integer" Valor: Integer;comenzar Ints := TARE.Integer.Crear()0, 1, 2, 3); TUtils.MakeAtLeast.Integer()Ints, 2); para Valor dentro Ints do WriteLn()Valor); ReadLn;final.

Al igual que con 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 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.

Genericidad en Free Pascal

Free Pascal implementó genéricos antes que Delphi, y con diferente sintaxis y semántica. 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, los programadores de Free Pascal pueden usar genéricos en el estilo que prefieran.

Ejemplo de Delphi y Free Pascal:

// Estilo Delphiunidad A;{$ifdef fpc} {$mode delphi}{$endif}interfazTipo TGenericClass.T = clase función Foo()const AValue: T): T; final;aplicaciónfunción TGenericClass.T.Foo()const AValue: T): T;comenzar Resultado := AValue + AValue;final;final.// Estilo ObjFPC de Pascal Libreunidad B;{$ifdef fpc} {$mode objfpc}{$endif}interfazTipo genérico genérico TGenericClass.T = clase función Foo()const AValue: T): T; final;aplicaciónfunción TGenericClass.Foo()const AValue: T): T;comenzar Resultado := AValue + AValue;final;final.// uso de ejemplo, estilo Delphiprograma TestGenDelphi;{$ifdef fpc} {$mode delphi}{$endif}uso A,B;Var GC1: A.TGenericClass.Integer" GC2: B.TGenericClass.String"comenzar GC1 := A.TGenericClass.Integer.Crear; GC2 := B.TGenericClass.String.Crear; WriteLn()GC1.Foo()100); // 200 WriteLn()GC2.Foo()'hola '); // holahello GC1.Gratis; GC2.Gratis;final.// uso de ejemplo, estilo ObjFPCprograma TestGenDelphi;{$ifdef fpc} {$mode objfpc}{$endif}uso A,B;// requerido en ObjFPCTipo TAGenericClass Int = especialización A.TGenericClass.Integer" TBGenericClassString = especialización B.TGenericClass.String"Var GC1: TAGenericClass Int; GC2: TBGenericClassString;comenzar GC1 := TAGenericClass Int.Crear; GC2 := TBGenericClassString.Crear; WriteLn()GC1.Foo()100); // 200 WriteLn()GC2.Foo()'hola '); // holahello GC1.Gratis; GC2.Gratis;final.

Lenguajes funcionales

Genericidad en Haskell

El mecanismo de clase de tipo de Haskell admite la programación genérica. Seis de las clases de tipos predefinidas en Haskell (incluidos Eq, los tipos cuya igualdad se puede comparar, 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 define un nuevo tipo puede establecer que este tipo será una instancia de una de estas clases de tipos especiales, sin proporcionar implementaciones de los métodos de clase tal como están. suele ser necesario cuando se declaran instancias de clase. Todos los métodos necesarios serán "derivados" – es decir, construido automáticamente – basado en la estructura del tipo. Por ejemplo, la siguiente declaración de un tipo de árboles binarios establece que debe ser una instancia de las clases Eq y Show:

datos BinTree a = Leaf a Silencio Node ()BinTree a) a ()BinTree a) Conduciendo ()Eq, Show)

Esto da como resultado una función de igualdad (==) y una función de representación de cadenas (show) que se definen automáticamente para cualquier tipo de la forma BinTree T siempre que T admita esas operaciones.

El soporte para instancias derivadas de Eq y Show hace que sus métodos == y show sean genéricos en un cualitativamente diferente de 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 las 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 de Haskell (discutido a continuación).

PolyP

PolyP fue la primera extensión del lenguaje de programación genérico de 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 pueden definirse mediante inducción estructural sobre la estructura del funtor patrón de un tipo de datos regular. Los tipos de datos regulares en PolyP son un subconjunto de los tipos de datos de Haskell. Un tipo de datos regular t debe ser del tipo * → *, y si a es el argumento de tipo formal en la definición, entonces todas las llamadas recursivas a t debe tener la forma t a. Estas restricciones descartan los tipos de datos de orden superior, así como los tipos de datos anidados, donde las llamadas recursivas tienen una forma diferente. La función flatten en PolyP se proporciona aquí como ejemplo:

 planos :: Recursos ordinarios d = d a - [a] planos = cata fl politípico fl :: f a [a] - [a] Caso f de g+h - o fl fl g*h - ()x,Sí.) - fl x ++ fl Sí. () - x - [] Par - x - [x] Rec - x - x d@g - concat . planos . Pág. fl Con t - x - [] cata :: Recursos ordinarios d = ()FunctorOf d a b - b) - d a - b
Haskell genérico

El Haskell genérico es otra extensión de Haskell, desarrollado en la Universidad de Utrecht en los Países Bajos. Las extensiones que ofrece son:

  • Valores tipo índice se define como un valor indexado sobre los diversos constructores tipo Haskell (unidad, tipos primitivos, sumas, productos y constructores tipo definidos por el usuario). Además, también podemos especificar el comportamiento de un valor de tipo indexado para un constructor específico utilizando constructores, y reutilizar una definición genérica en otra Casos predeterminados.

El valor indexado por tipo resultante se puede especializar en cualquier tipo.

  • Tipos de tipo son tipos indexados sobre tipos, definidos por dar un caso para ambos * y k → k '. Las incidencias se obtienen aplicando el tipo de tipo tipo indicado a un tipo.
  • Las definiciones genéricas se pueden utilizar aplicando a un tipo o tipo. Esto se llama aplicación genérica. El resultado es un tipo o valor, dependiendo de qué tipo de definición genérica se aplique.
  • abstracción genérica permite definir definiciones genéricas mediante la abstracción de un parámetro tipo (de un tipo determinado).
  • Tipos de índice son tipos que se indexan sobre los constructores tipo. Estos pueden utilizarse para dar tipos a valores genéricos más involucrados. Los tipos de tipo indicados pueden ser especializados en cualquier tipo.

Como ejemplo, la función de igualdad en Generic Haskell:

 Tipo Eq [ * ] t1 t2 = t1 - t2 - Bool Tipo Eq [ k - l ] t1 t2 = para todos u1 u2. Eq [ k ] u1 u2 - Eq [ l ] ()t1 u1) ()t2 u2) eq {}Silencio t :: k Silencio} :: Eq [ k ] t t eq {}Silencio Dependencia Silencio} ¿Qué? ¿Qué? = Cierto. eq {}Silencio :+: Silencio} eqA eqB ()Inl a1) ()Inl a2) = eqA a1 a2 eq {}Silencio :+: Silencio} eqA eqB ()Inr b1) ()Inr b2) = eqB b1 b2 eq {}Silencio :+: Silencio} eqA eqB ¿Qué? ¿Qué? = Falso eq {}Silencio :* Silencio} eqA eqB ()a1 :* b1) ()a2 :* b2) = eqA a1 a2 " eqB b1 b2 eq {}Silencio Int Silencio} = ()==) eq {}Silencio Char Silencio} = ()==) eq {}Silencio Bool Silencio} = ()==)

Limpiar

Clean ofrece programación genérica basada en § PolyP y § Generic Haskell con el respaldo de GHC ≥ 6.0. Parametriza por tipo como los mas ofrece sobrecarga.

Otros idiomas

Los lenguajes de la familia ML admiten la programación genérica mediante 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 que se asignan sus valores reales al crear una instancia del módulo. Un ejemplo es una matriz de registros genérica donde el ancho de la matriz se proporciona a través de un parámetro. Una matriz de este tipo, combinada con un vector de cable genérico, puede crear un búfer genérico o un módulo de memoria con un ancho de bits arbitrario a partir de la implementación de un solo módulo.

VHDL, derivado de Ada, también tiene capacidades genéricas.

C admite "expresiones genéricas de tipo" usando el _Generic palabra clave:

#define cbrt(x) _Generic(x), long double: cbrtl,  default: cbrt,  flotador: cbrtf)(x)

Contenido relacionado

Evaluación perezosa

Puerto serial

Corrección gamma

Más resultados...
Tamaño del texto:
undoredo
format_boldformat_italicformat_underlinedstrikethrough_ssuperscriptsubscriptlink
save