Programação genérica - Generic programming

Programação genérica é um estilo de programação de computador no qual algoritmos são escritos em termos de tipos a serem especificados posteriormente que são instanciados quando necessário para tipos específicos fornecidos como parâmetros . Essa abordagem, iniciada pela linguagem de programação ML em 1973, permite escrever funções ou tipos comuns que diferem apenas no conjunto de tipos em que operam quando usados, reduzindo assim a duplicação . Essas entidades de software são conhecidas como genéricos em Ada , C # , Delphi , Eiffel , F # , Java , Nim , Python , Rust , Swift , TypeScript e Visual Basic .NET . Eles são conhecidos como polimorfismo paramétrico em ML , Scala , Julia e Haskell (a comunidade Haskell também usa o termo "genérico" para um conceito relacionado, mas um tanto diferente); modelos em C ++ e D ; e tipos parametrizados no influente livro de 1994 Design Patterns .

O termo "programação genérica" ​​foi originalmente cunhado por David Musser e Alexander Stepanov em um sentido mais específico do que o anterior, para descrever um paradigma de programação em que os requisitos fundamentais dos tipos são abstraídos de exemplos concretos de algoritmos e estruturas de dados e formalizados como conceitos , com funções genéricas implementadas em termos desses conceitos, normalmente usando mecanismos de genericidade de linguagem conforme descrito acima.

Stepanov – Musser e outros paradigmas de programação genéricos

A programação genérica é definida em Musser & Stepanov (1989) da seguinte forma,

A programação genérica gira em torno da ideia de abstrair algoritmos concretos e eficientes para obter algoritmos genéricos que podem ser combinados com diferentes representações de dados para produzir uma ampla variedade de softwares úteis.

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

O paradigma de "programação genérica" ​​é uma abordagem à decomposição de software em que os requisitos fundamentais dos tipos são abstraídos de exemplos concretos de algoritmos e estruturas de dados e formalizados como conceitos , analogamente à abstração de teorias algébricas em álgebra abstrata . Os primeiros exemplos dessa abordagem de programação foram implementados em Scheme e Ada, embora o exemplo mais conhecido seja a Standard Template Library (STL), que desenvolveu uma teoria de iteradores que é usada para desacoplar estruturas de dados de sequência e os algoritmos que operam nelas.

Por exemplo, dadas N estruturas de dados de sequência, por exemplo, lista unida individualmente, vetor etc., e algoritmos M para operar neles, por exemplo find, sortetc., uma abordagem direta implementaria cada algoritmo especificamente para cada estrutura de dados, dando N × M combinações para implemento. No entanto, na abordagem de programação genérica, cada estrutura de dados retorna um modelo de um conceito de iterador (um tipo de valor simples que pode ser desreferenciado para recuperar o valor atual ou alterado para apontar para outro valor na sequência) e cada algoritmo é, em vez disso, escrito genericamente com argumentos de tais iteradores, por exemplo, um par de iteradores apontando para o início e o fim da subsequência ou intervalo a processar. Assim, apenas N + M combinações de estrutura de dados-algoritmo precisam ser implementadas. Vários conceitos de iterador são especificados no STL, cada um um refinamento de conceitos mais restritivos, por exemplo, iteradores de encaminhamento apenas fornecem movimento para o próximo valor em uma sequência (por exemplo, adequado para uma lista unida individualmente ou um fluxo de dados de entrada), enquanto um acesso aleatório iterador também fornece acesso direto em tempo constante a qualquer elemento da sequência (por exemplo, adequado para um vetor). Um ponto importante é que uma estrutura de dados retornará um modelo do conceito mais geral que pode ser implementado com eficiência - os requisitos de complexidade computacional são explicitamente parte da definição do conceito. Isso limita as estruturas de dados às quais um determinado algoritmo pode ser aplicado e tais requisitos de complexidade são um dos principais determinantes da escolha da estrutura de dados. A programação genérica foi aplicada de forma semelhante em outros domínios, por exemplo, algoritmos de gráfico.

Observe que, embora essa abordagem frequentemente utilize recursos de linguagem de genericidade / modelos de tempo de compilação , ela é, na verdade, independente de detalhes técnicos de linguagem específicos. O pioneiro da programação genérica Alexander Stepanov escreveu:

A programação genérica trata de abstrair e classificar algoritmos e estruturas de dados. Ele se inspira em Knuth e não na teoria dos tipos. Seu objetivo é a construção incremental de catálogos sistemáticos de algoritmos e estruturas de dados úteis, eficientes e abstratos. Tal empreendimento ainda é um sonho.

-  Alexander Stepanov, Breve História do STL

Eu acredito que as teorias dos iteradores são tão centrais para a Ciência da Computação quanto as teorias dos anéis ou espaços de Banach são centrais para a Matemática.

-  Alexander Stepanov, uma entrevista com A. Stepanov

Bjarne Stroustrup observou,

Seguindo Stepanov, podemos definir a programação genérica sem mencionar os recursos da linguagem: Levante algoritmos e estruturas de dados de exemplos concretos para sua forma mais geral e abstrata.

-  Bjarne Stroustrup, evoluindo uma linguagem no e para o mundo real: C ++ 1991-2006

Outros paradigmas de programação que foram descritos como programação genérica incluem a programação genérica de tipo de dados, conforme descrito em "Programação genérica - uma introdução". A abordagem Scrap your boilerplate é uma abordagem de programação genérica leve para Haskell.

Neste artigo, distinguimos os paradigmas de programação de alto nível da programação genérica , acima, dos mecanismos de genericidade da linguagem de programação de nível inferior usados ​​para implementá-los (consulte Suporte à linguagem de programação para genericidade ). Para mais discussão e comparação de paradigmas de programação genéricos, consulte.

Suporte à linguagem de programação para genericidade

Instalações genericidade ter existido em linguagens de alto nível, pelo menos desde a década de 1970 em linguagens como ML , CLU e Ada , e foram posteriormente adotado por muitos objeto baseados e orientados a objeto idiomas, incluindo BETA , C ++ , D , Eiffel , Java , e DEC agora é a linguagem Trellis-Owl extinta .

A genericidade é implementada e suportada de forma diferente em várias linguagens de programação; o termo "genérico" também foi usado de forma diferente em vários contextos de programação. Por exemplo, em Forth, o compilador pode executar código durante a compilação e pode-se criar novas palavras - chave do compilador e novas implementações para essas palavras em tempo real. Possui poucas palavras que expõem o comportamento do compilador e, portanto, naturalmente oferece capacidades de genericidade que, no entanto, não são referidas como tal na maioria dos textos do Forth. Da mesma forma, linguagens digitadas dinamicamente, especialmente aquelas interpretadas, geralmente oferecem genericidade por padrão, pois tanto a passagem de valores para funções quanto a atribuição de valores são indiferentes ao tipo e tal comportamento é frequentemente utilizado para abstração ou concisão de código, no entanto, isso não é normalmente rotulado de genericidade , pois é um conseqüência direta do sistema de tipagem dinâmica empregado pelo idioma. O termo tem sido usado em programação funcional , especificamente em linguagens do tipo Haskell , que usam um sistema de tipo estrutural onde os tipos são sempre paramétricos e o código real desses tipos é genérico. Esses usos ainda têm um propósito semelhante de salvar código e renderizar uma abstração.

Arrays e structs podem ser vistos como tipos genéricos predefinidos. Cada uso de uma matriz ou tipo de estrutura instancia um novo tipo concreto ou reutiliza um tipo instanciado anterior. Tipos de elemento de matriz e tipos de elemento de estrutura são tipos parametrizados, que são usados ​​para instanciar o tipo genérico correspondente. Tudo isso geralmente está embutido no compilador e a sintaxe difere de outras construções genéricas. Algumas linguagens de programação extensíveis tentam unificar tipos genéricos internos e definidos pelo usuário.

Segue um amplo levantamento dos mecanismos de genericidade em linguagens de programação. Para uma pesquisa específica comparando a adequação de mecanismos para programação genérica, consulte.

Em linguagens orientadas a objetos

Ao criar classes de contêiner em linguagens com tipos estáticos, é inconveniente escrever implementações específicas para cada tipo de dados contido, especialmente se o código de cada tipo de dados for virtualmente idêntico. Por exemplo, em C ++, essa duplicação de código pode ser contornada definindo um modelo de classe:

template<typename T>
class List {
  // Class contents.
};

List<Animal> list_of_animals;
List<Car> list_of_cars;

Acima, Té um espaço reservado para qualquer tipo que é especificado quando a lista é criada. Esses "containers-of-type-T", comumente chamados de modelos , permitem que uma classe seja reutilizada com diferentes tipos de dados, desde que certos contratos, como subtipos e assinatura, sejam mantidos. Este mecanismo de genericidade não deve ser confundido com polimorfismo de inclusão , que é o uso algorítmico de subclasses trocáveis: por exemplo, uma lista de objetos do tipo Moving_Objectcontendo objetos do tipo Animale Car. Os modelos também podem ser usados ​​para funções independentes de tipo, como no Swapexemplo abaixo:

// "&" denotes a reference
template<typename T>
void Swap(T& a, T& b) { // A similar, but safer and potentially faster function 
                        // is defined in the standard library header <utility>
  T temp = b;
  b = a;
  a = temp;
}

std::string world = "World!";
std::string hello = "Hello, ";
Swap(world, hello);
std::cout << world << hello << ‘\n;  // Output is "Hello, World!".

A templateconstrução C ++ usada acima é amplamente citada como a construção de genericidade que popularizou a noção entre programadores e designers de linguagem e oferece suporte a muitos idiomas de programação genéricos. A linguagem de programação D também oferece modelos totalmente genéricos baseados no precedente C ++, mas com uma sintaxe simplificada. A linguagem de programação Java forneceu recursos de genericidade sintaticamente baseados em C ++ desde a introdução do J2SE 5.0.

C # 2.0, Oxygene 1.5 (também conhecido como Chrome) e Visual Basic .NET 2005 têm construções que tiram proveito do suporte para genéricos presente no Microsoft .NET Framework desde a versão 2.0.

Genéricos em Ada

Ada tem genéricos desde que foi projetado pela primeira vez em 1977-1980. A biblioteca padrão usa genéricos para fornecer muitos serviços. Ada 2005 adiciona uma biblioteca de contêineres genérica abrangente à biblioteca padrão, que foi inspirada na biblioteca de modelos padrão do C ++ .

Uma unidade genérica é um pacote ou subprograma que assume um ou mais parâmetros formais genéricos .

Um parâmetro formal genérico é um valor, uma variável, uma constante, um tipo, um subprograma ou mesmo uma instância de outra unidade genérica designada. Para tipos formais genéricos, a sintaxe distingue entre tipos discretos, de ponto flutuante, de ponto fixo, de acesso (ponteiro), etc. Alguns parâmetros formais podem ter valores padrão.

Para instanciar uma unidade genérica, o programador passa parâmetros reais para cada formal. A instância genérica então se comporta como qualquer outra unidade. É possível instanciar unidades genéricas em tempo de execução , por exemplo, dentro de um loop.

Exemplo

A especificação de um pacote genérico:

 generic
    Max_Size : Natural; -- a generic formal value
    type Element_Type is private; -- a generic formal type; accepts any nonlimited type
 package Stacks is
    type Size_Type is range 0 .. Max_Size;
    type Stack is limited private;
    procedure Create (S : out Stack;
                      Initial_Size : in Size_Type := Max_Size);
    procedure Push (Into : in out Stack; Element : in Element_Type);
    procedure Pop (From : in out Stack; Element : out Element_Type);
    Overflow : exception;
    Underflow : exception;
 private
    subtype Index_Type is Size_Type range 1 .. Max_Size;
    type Vector is array (Index_Type range <>) of Element_Type;
    type Stack (Allocated_Size : Size_Type := 0) is record
       Top : Index_Type;
       Storage : Vector (1 .. Allocated_Size);
    end record;
 end Stacks;

Instanciando o pacote genérico:

 type Bookmark_Type is new Natural;
 -- records a location in the text document we are editing

 package Bookmark_Stacks is new Stacks (Max_Size => 20,
                                        Element_Type => Bookmark_Type);
 -- Allows the user to jump between recorded locations in a document

Usando uma instância de um pacote genérico:

 type Document_Type is record
    Contents : Ada.Strings.Unbounded.Unbounded_String;
    Bookmarks : Bookmark_Stacks.Stack;
 end record;

 procedure Edit (Document_Name : in String) is
   Document : Document_Type;
 begin
   -- Initialise the stack of bookmarks:
   Bookmark_Stacks.Create (S => Document.Bookmarks, Initial_Size => 10);
   -- Now, open the file Document_Name and read it in...
 end Edit;
Vantagens e limitações

A sintaxe da linguagem permite a especificação precisa de restrições em parâmetros formais genéricos. Por exemplo, é possível especificar que um tipo formal genérico aceitará apenas um tipo modular como real. Também é possível expressar restrições entre parâmetros formais genéricos; por exemplo:

 generic
    type Index_Type is (<>); -- must be a discrete type
    type Element_Type is private; -- can be any nonlimited type
    type Array_Type is array (Index_Type range <>) of Element_Type;

Neste exemplo, Array_Type é restringido por Index_Type e Element_Type. Ao instanciar a unidade, o programador deve passar um tipo de array real que satisfaça essas restrições.

A desvantagem desse controle refinado é uma sintaxe complicada, mas, como todos os parâmetros formais genéricos são completamente definidos na especificação, o compilador pode instanciar os genéricos sem olhar para o corpo do genérico.

Ao contrário do C ++, Ada não permite instâncias genéricas especializadas e requer que todos os genéricos sejam instanciados explicitamente. Essas regras têm várias consequências:

  • o compilador pode implementar genéricos compartilhados : o código-objeto de uma unidade genérica pode ser compartilhado entre todas as instâncias (a menos que o programador solicite a linha de subprogramas, é claro). Como outras consequências:
    • não há possibilidade de inchaço de código (o inchaço de código é comum em C ++ e requer cuidados especiais, conforme explicado a seguir).
    • é possível instanciar genéricos em tempo de execução, bem como em tempo de compilação, uma vez que nenhum novo código de objeto é necessário para uma nova instância.
    • os objetos reais correspondentes a um objeto formal genérico são sempre considerados não estáticos dentro do genérico; consulte Objetos formais genéricos no Wikilivro para obter detalhes e consequências.
  • todas as instâncias de um genérico sendo exatamente iguais, é mais fácil revisar e entender programas escritos por outros; não há "casos especiais" a serem considerados.
  • sendo todas as instanciações explícitas, não há instanciações ocultas que possam dificultar a compreensão do programa.
  • Ada não permite "metaprogramação de template", porque não permite especializações.

Modelos em C ++

C ++ usa modelos para habilitar técnicas de programação genéricas. A C ++ Standard Library inclui a Standard Template Library ou STL, que fornece uma estrutura de modelos para algoritmos e estruturas de dados comuns. Os modelos em C ++ também podem ser usados ​​para metaprogramação de modelo , que é uma maneira de pré-avaliar parte do código em tempo de compilação, em vez de tempo de execução . Usando a especialização de template, os Templates C ++ são considerados Turing completos .

Visão geral técnica

Existem muitos tipos de modelos, sendo os mais comuns modelos de função e modelos de classe. Um modelo de função é um padrão para criar funções comuns com base nos tipos de parametrização fornecidos quando instanciados. Por exemplo, a Biblioteca de modelos padrão C ++ contém o modelo de função max(x, y)que cria funções que retornam x ou y, o que for maior. max()pode ser definido assim:

template<typename T>
T max(T x, T y) {
  return x < y ? y : x;
}

As especializações desse modelo de função, instanciações com tipos específicos, podem ser chamadas como uma função comum:

std::cout << max(3, 7);  // Outputs 7.

O compilador examina os argumentos usados ​​para chamar maxe determina que se trata de uma chamada para max(int, int). Ele então instancia uma versão da função onde o tipo de parametrização Té int, fazendo o equivalente da seguinte função:

int max(int x, int y) {
  return x < y ? y : x;
}

Isso funciona se os argumentos xe yforem inteiros, strings ou qualquer outro tipo para o qual a expressão x < yseja sensível ou, mais especificamente, para qualquer tipo para o qual operator<seja definido. A herança comum não é necessária para o conjunto de tipos que podem ser usados ​​e, portanto, é muito semelhante à digitação de pato . Um programa que define um tipo de dados personalizado pode usar a sobrecarga do operador para definir o significado <desse tipo, permitindo assim seu uso com o max()modelo de função. Embora isso possa parecer um benefício menor neste exemplo isolado, no contexto de uma biblioteca abrangente como a STL, permite que o programador obtenha funcionalidade extensa para um novo tipo de dados, apenas definindo alguns operadores para ele. A mera definição <permite que um tipo seja usado com os algoritmos padrão sort(), stable_sort()e binary_search()ou seja colocado dentro de estruturas de dados como sets, heaps e matrizes associativas .

Os modelos C ++ são completamente seguros em tempo de compilação. Como demonstração, o tipo padrão complexnão define o <operador, porque não há uma ordem estrita nos números complexos . Portanto, max(x, y)vai falhar com um erro de compilação, se x e y são complexvalores. Da mesma forma, outros modelos baseados em <não podem ser aplicados aos complexdados, a menos que uma comparação (na forma de um functor ou função) seja fornecida. Por exemplo: A complexnão pode ser usado como chave para a, a mapmenos que uma comparação seja fornecida. Infelizmente, os compiladores geram historicamente mensagens de erro um tanto esotéricas, longas e inúteis para esse tipo de erro. Garantir que um determinado objeto adere a um protocolo de método pode aliviar esse problema. Os idiomas que usam em comparevez de <também podem usar complexvalores como chaves.

Outro tipo de modelo, um modelo de classe, estende o mesmo conceito às classes. Uma especialização de modelo de classe é uma classe. Os modelos de classe costumam ser usados ​​para fazer contêineres genéricos. Por exemplo, o STL possui um contêiner de lista vinculada . Para fazer uma lista vinculada de inteiros, escreve-se list<int>. Uma lista de strings é indicada list<string>. A listpossui um conjunto de funções padrão associadas a ele, que funcionam para qualquer tipo de parametrização compatível.

Especialização de modelo

Um recurso poderoso dos modelos de C ++ é a especialização de modelos . Isso permite que implementações alternativas sejam fornecidas com base em certas características do tipo parametrizado que está sendo instanciado. A especialização de modelo tem dois propósitos: permitir certas formas de otimização e reduzir o inchaço do código.

Por exemplo, considere uma sort()função de modelo. Uma das principais atividades dessa função é trocar ou trocar os valores em duas das posições do contêiner. Se os valores forem grandes (em termos do número de bytes necessários para armazenar cada um deles), então geralmente é mais rápido construir primeiro uma lista separada de ponteiros para os objetos, classificar esses ponteiros e, em seguida, construir a sequência de classificação final . Se os valores forem muito pequenos, no entanto, geralmente é mais rápido apenas trocar os valores no local conforme necessário. Além disso, se o tipo parametrizado já for de algum tipo de ponteiro, não há necessidade de construir uma matriz de ponteiro separada. A especialização do modelo permite que o criador do modelo escreva diferentes implementações e especifique as características que o (s) tipo (s) parametrizado (s) deve (m) ter para cada implementação a ser usada.

Ao contrário dos modelos de função, os modelos de classe podem ser parcialmente especializados . Isso significa que uma versão alternativa do código do modelo de classe pode ser fornecida quando alguns dos parâmetros do modelo são conhecidos, deixando outros parâmetros do modelo genéricos. Isso pode ser usado, por exemplo, para criar uma implementação padrão (a especialização primária ) que assume que copiar um tipo de parametrização é caro e, em seguida, criar especializações parciais para tipos que são baratos para copiar, aumentando assim a eficiência geral. Os clientes desse modelo de classe apenas usam as especializações dele, sem precisar saber se o compilador usou a especialização primária ou alguma especialização parcial em cada caso. Os modelos de classe também podem ser totalmente especializados, o que significa que uma implementação alternativa pode ser fornecida quando todos os tipos de parametrização são conhecidos.

Vantagens e desvantagens

Alguns usos de modelos, como a max()função, foram previamente preenchidos por macros de pré - processador semelhantes a funções (um legado da linguagem de programação C ). Por exemplo, aqui está uma possível implementação dessa macro:

#define max(a,b) ((a) < (b) ? (b) : (a))

As macros são expandidas (cópia colada) pelo pré-processador , antes da compilação adequada; os modelos são funções reais reais. As macros são sempre expandidas em linha; os modelos também podem ser funções embutidas quando o compilador considerar apropriado.

No entanto, os modelos são geralmente considerados uma melhoria em relação às macros para esses fins. Os modelos são seguros para o tipo. Os modelos evitam alguns dos erros comuns encontrados no código que faz uso intenso de macros semelhantes a funções, como avaliar parâmetros com efeitos colaterais duas vezes. Talvez o mais importante, os modelos foram projetados para serem aplicáveis ​​a problemas muito maiores do que as macros.

Existem quatro desvantagens principais no uso de modelos: recursos compatíveis, suporte do compilador, mensagens de erro ruins (geralmente com pré C ++ 20 SFINAE) e inchaço de código :

  1. Os modelos em C ++ carecem de muitos recursos, o que torna muitas vezes impossível implementá-los e usá-los de maneira direta. Em vez disso, os programadores têm que confiar em truques complicados que levam a códigos inchados, difíceis de entender e de manter. Os desenvolvimentos atuais nos padrões C ++ exacerbam esse problema, fazendo uso intenso desses truques e criando muitos novos recursos para modelos neles ou com eles em mente.
  2. Muitos compiladores historicamente tinham suporte pobre para modelos, portanto, o uso de modelos poderia ter tornado o código um pouco menos portátil. O suporte também pode ser pobre quando um compilador C ++ está sendo usado com um vinculador que não reconhece C ++ ou quando se tenta usar modelos através dos limites da biblioteca compartilhada .
  3. Os compiladores podem produzir mensagens de erro confusas, longas e às vezes inúteis quando são detectados erros no código que usa SFINAE. Isso pode dificultar o desenvolvimento dos modelos.
  4. Finalmente, o uso de modelos requer que o compilador gere uma instância separada da classe ou função modelada para cada permutação de parâmetros de tipo usados ​​com ele. (Isso é necessário porque os tipos em C ++ não são todos do mesmo tamanho e os tamanhos dos campos de dados são importantes para o funcionamento das classes.) Portanto, o uso indiscriminado de modelos pode levar ao inchaço do código , resultando em executáveis ​​excessivamente grandes. No entanto, o uso criterioso de especialização e derivação de modelo pode reduzir drasticamente esse inchaço de código em alguns casos:

Portanto, a derivação pode ser usada para reduzir o problema de código replicado porque são usados ​​modelos? Isso envolveria derivar um modelo de uma classe comum. Essa técnica foi bem-sucedida em reduzir o inchaço do código no uso real. Pessoas que não usam uma técnica como essa descobriram que o código replicado pode custar megabytes de espaço de código, mesmo em programas de tamanho moderado.

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

As instanciações extras geradas por modelos também podem fazer com que alguns depuradores tenham dificuldade em trabalhar normalmente com modelos. Por exemplo, definir um ponto de interrupção de depuração em um modelo de um arquivo de origem pode deixar de definir o ponto de interrupção na instanciação real desejada ou pode definir um ponto de interrupção em cada lugar em que o modelo é instanciado.

Além disso, o código-fonte de implementação do modelo deve estar completamente disponível (por exemplo, incluído em um cabeçalho) para a unidade de tradução (arquivo-fonte) que o usa. Os modelos, incluindo grande parte da Biblioteca Padrão, se não incluídos nos arquivos de cabeçalho, não podem ser compilados. (Isso está em contraste com o código não modelado, que pode ser compilado para binário, fornecendo apenas um arquivo de cabeçalho de declarações para o código que o usa.) Isso pode ser uma desvantagem ao expor o código de implementação, que remove algumas abstrações e pode restringir sua uso em projetos de código fechado.

Modelos em D

A linguagem de programação D oferece suporte a modelos baseados em design em C ++. A maioria das expressões idiomáticas de template C ++ serão transportadas para D sem alteração, mas D adiciona algumas funcionalidades adicionais:

  • Os parâmetros de modelo em D não são restritos apenas a tipos e valores primitivos (como era em C ++ antes de C ++ 20), mas também permitem valores de tempo de compilação arbitrários (como strings e literais de estrutura) e aliases para identificadores arbitrários, incluindo outros modelos ou instanciações de modelo.
  • As restrições de modelo e a static ifinstrução fornecem uma alternativa aos conceitos C ++ de C ++ e if constexpr.
  • A is(...)expressão permite a instanciação especulativa para verificar as características de um objeto em tempo de compilação.
  • A autopalavra-chave e a typeofexpressão permitem inferência de tipo para declarações de variáveis ​​e valores de retorno de funções, que por sua vez permitem "tipos de Voldemort" (tipos que não têm um nome global).

Modelos em D utilizar uma sintaxe diferente do que em C ++: Considerando que em parâmetros de modelo C ++ são envoltos em suportes angulares ( Template<param1, param2>), D utiliza um sinal de exclamação e parênteses: Template!(param1, param2). Isso evita as dificuldades de análise do C ++ devido à ambigüidade com os operadores de comparação. Se houver apenas um parâmetro, os parênteses podem ser omitidos.

Convencionalmente, D combina os recursos acima para fornecer polimorfismo em tempo de compilação usando programação genérica baseada em características. Por exemplo, um intervalo de entrada é definido como qualquer tipo que satisfaça as verificações realizadas por isInputRange, que é definido da seguinte forma:

template isInputRange(R)
{
    enum bool isInputRange = is(typeof(
    (inout int = 0)
    {
        R r = R.init;     // can define a range object
        if (r.empty) {}   // can test for empty
        r.popFront();     // can invoke popFront()
        auto h = r.front; // can get the front of the range
    }));
}

Uma função que aceita apenas intervalos de entrada pode usar o modelo acima em uma restrição de modelo:

auto fun(Range)(Range range)
    if (isInputRange!Range)
{
    // ...
}
Geração de código

Além da metaprogramação de modelo, D também fornece vários recursos para permitir a geração de código em tempo de compilação:

  • A importexpressão permite ler um arquivo do disco e usar seu conteúdo como uma expressão de string.
  • A reflexão em tempo de compilação permite enumerar e inspecionar declarações e seus membros durante a compilação.
  • Os atributos definidos pelo usuário permitem que os usuários anexem identificadores arbitrários às declarações, que podem então ser enumerados usando reflexão em tempo de compilação.
  • Compile-Time Function Execution (CTFE) permite que um subconjunto de D (restrito a operações seguras) seja interpretado durante a compilação.
  • Os mixins de string permitem avaliar e compilar o conteúdo de uma expressão de string como código D que se torna parte do programa.

Combinar o acima permite gerar código com base em declarações existentes. Por exemplo, as estruturas de serialização de D podem enumerar os membros de um tipo e gerar funções especializadas para cada tipo serializado para realizar a serialização e desserialização. Atributos definidos pelo usuário podem indicar regras de serialização.

A importexpressão e a execução da função em tempo de compilação também permitem a implementação eficiente de linguagens específicas de domínio . Por exemplo, dada uma função que recebe uma string contendo um modelo HTML e retorna o código-fonte D equivalente, é possível usá-la da seguinte maneira:

// Import the contents of example.htt as a string manifest constant.
enum htmlTemplate = import("example.htt");

// Transpile the HTML template to D code.
enum htmlDCode = htmlTemplateToD(htmlTemplate);

// Paste the contents of htmlDCode as D code.
mixin(htmlDCode);

Genericidade em Eiffel

Aulas genéricas fazem parte da Eiffel desde o método original e o design da linguagem. As publicações de fundação de Eiffel usam o termo genericidade para descrever a criação e o uso de classes genéricas.

Genericidade básica / irrestrita

As classes genéricas são declaradas com seu nome de classe e uma lista de um ou mais parâmetros genéricos formais . No código a seguir, a classe LISTtem um parâmetro genérico formalG

class
    LIST [G]
            ...
feature   -- Access
    item: G
            -- The item currently pointed to by cursor
            ...
feature   -- Element change
    put (new_item: G)
            -- Add `new_item' at the end of the list
            ...

Os parâmetros genéricos formais são marcadores de posição para nomes de classes arbitrárias que serão fornecidos quando uma declaração da classe genérica for feita, conforme mostrado nas duas derivações genéricas abaixo, onde ACCOUNTe DEPOSITsão outros nomes de classes. ACCOUNTe DEPOSITsão considerados parâmetros genéricos reais , pois fornecem nomes de classes reais para substituir Gno uso real.

    list_of_accounts: LIST [ACCOUNT]
            -- Account list

    list_of_deposits: LIST [DEPOSIT]
            -- Deposit list

No sistema de tipos de Eiffel, embora classe LIST [G]seja considerada uma classe, não é considerada um tipo. No entanto, uma derivação genérica de LIST [G]como LIST [ACCOUNT]é considerada um tipo.

Genericidade restrita

Para a classe de lista mostrada acima, um parâmetro genérico real que substitui Gpode ser qualquer outra classe disponível. Para restringir o conjunto de classes a partir do qual os parâmetros genéricos reais válidos podem ser escolhidos, uma restrição genérica pode ser especificada. Na declaração de classe SORTED_LISTabaixo, a restrição genérica determina que qualquer parâmetro genérico real válido será uma classe que herda da classe COMPARABLE. A restrição genérica garante que os elementos de a SORTED_LISTpossam de fato ser classificados.

class
    SORTED_LIST [G -> COMPARABLE]

Genéricos em Java

O suporte para os genéricos ou "containers-of-type-T" foi adicionado à linguagem de programação Java em 2004 como parte do J2SE 5.0. Em Java, os genéricos são verificados apenas no momento da compilação quanto à exatidão do tipo. As informações de tipo genérico são então removidas por meio de um processo chamado eliminação de tipo , para manter a compatibilidade com implementações JVM antigas, tornando-as indisponíveis no tempo de execução. Por exemplo, a List<String>é convertido no tipo bruto List. O compilador insere casts de tipo para converter os elementos para o Stringtipo quando eles são recuperados da lista, reduzindo o desempenho em comparação com outras implementações, como modelos C ++.

Genericidade em .NET [C #, VB.NET]

Os genéricos foram adicionados como parte do .NET Framework 2.0 em novembro de 2005, com base em um protótipo de pesquisa da Microsoft Research iniciada em 1999. Embora semelhantes aos genéricos em Java, os genéricos .NET não aplicam eliminação de tipo , mas implementam genéricos como um mecanismo de primeira classe no tempo de execução usando reificação . Essa escolha de design fornece funcionalidade adicional, como permitir a reflexão com preservação de tipos genéricos, bem como aliviar algumas das limitações de apagamento (como a impossibilidade de criar matrizes genéricas). Isso também significa que não há impacto no desempenho de conversões em tempo de execução e conversões de boxe normalmente caras . Quando os tipos primitivos e de valor são usados ​​como argumentos genéricos, eles obtêm implementações especializadas, permitindo coleções e métodos genéricos eficientes . Como em C ++ e Java, tipos genéricos aninhados como Dictionary <string, List <int>> são tipos válidos, no entanto, são desaconselhados para assinaturas de membro nas regras de design de análise de código.

O .NET permite seis variedades de restrições de tipo genérico usando a wherepalavra - chave, incluindo a restrição de tipos genéricos para serem tipos de valor, para serem classes, para ter construtores e para implementar interfaces. Abaixo está um exemplo com uma restrição de interface:

using System;

class Sample
{
    static void Main()
    {
        int[] array = { 0, 1, 2, 3 };
        MakeAtLeast<int>(array, 2); // Change array to { 2, 2, 2, 3 }
        foreach (int i in array)
            Console.WriteLine(i); // Print results.
        Console.ReadKey(true);
    }

    static void MakeAtLeast<T>(T[] list, T lowest) where T : IComparable<T>
    {
        for (int i = 0; i < list.Length; i++)
            if (list[i].CompareTo(lowest) < 0)
                list[i] = lowest;
    }
}

O MakeAtLeast()método permite a operação em arrays, com elementos do tipo genérico T. A restrição de tipo do método indica que o método é aplicável a qualquer tipo Tque implemente a IComparable<T>interface genérica . Isso garante um erro de tempo de compilação , se o método for chamado se o tipo não suportar comparação. A interface fornece o método genérico CompareTo(T).

O método acima também pode ser escrito sem tipos genéricos, simplesmente usando o Arraytipo não genérico . No entanto, como os arrays são contravariantes , a conversão não seria segura para o tipo e o compilador não seria capaz de encontrar certos erros possíveis que, de outra forma, seriam detectados ao usar tipos genéricos. Além disso, o método precisaria acessar os itens da matriz como objects, em vez disso, e exigiria conversão para comparar dois elementos. (Para tipos de valor, como tipos como inteste, requer uma conversão de box , embora isso possa ser contornado usando a Comparer<T>classe, como é feito nas classes de coleção padrão.)

Um comportamento notável de membros estáticos em uma classe .NET genérica é a instanciação de membro estático por tipo de tempo de execução (consulte o exemplo abaixo).

    //A generic class
    public class GenTest<T>
    {
        //A static variable - will be created for each type on reflection
        static CountedInstances OnePerType = new CountedInstances();

        //a data member
        private T mT;

        //simple constructor
        public GenTest(T pT)
        {
            mT = pT;
        }
    }

    //a class
    public class CountedInstances
    {
        //Static variable - this will be incremented once per instance
        public static int Counter;

        //simple constructor
        public CountedInstances()
        {
            //increase counter by one during object instantiation
            CountedInstances.Counter++;
        }
    }

  //main code entry point
  //at the end of execution, 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);

Genericidade em Delphi

O dialeto Object Pascal do Delphi adquiriu genéricos na versão Delphi 2007, inicialmente apenas com o (agora descontinuado) compilador .NET antes de ser adicionado ao código nativo na versão 2009 do Delphi. A semântica e os recursos dos genéricos Delphi são amplamente modelados nos genéricos do .NET 2.0, embora a implementação seja por necessidade bastante diferente. Aqui está uma tradução mais ou menos direta do primeiro exemplo C # mostrado acima:

program Sample;

{$APPTYPE CONSOLE}

uses
  Generics.Defaults; //for IComparer<>

type
  TUtils = class
    class procedure MakeAtLeast<T>(Arr: TArray<T>; const Lowest: T;
      Comparer: IComparer<T>); overload;
    class procedure MakeAtLeast<T>(Arr: TArray<T>; const Lowest: T); overload;
  end;

class procedure TUtils.MakeAtLeast<T>(Arr: TArray<T>; const Lowest: T;
  Comparer: IComparer<T>);
var
  I: Integer;
begin
  if Comparer = nil then Comparer := TComparer<T>.Default;
  for I := Low(Arr) to High(Arr) do
    if Comparer.Compare(Arr[I], Lowest) < 0 then
      Arr[I] := Lowest;
end;

class procedure TUtils.MakeAtLeast<T>(Arr: TArray<T>; const Lowest: T);
begin
  MakeAtLeast<T>(Arr, Lowest, nil);
end;

var
  Ints: TArray<Integer>;
  Value: Integer;
begin
  Ints := TArray<Integer>.Create(0, 1, 2, 3);
  TUtils.MakeAtLeast<Integer>(Ints, 2);
  for Value in Ints do
    WriteLn(Value);
  ReadLn;
end.

Assim como no C #, os métodos e também os tipos inteiros podem ter um ou mais parâmetros de tipo. No exemplo, TArray é um tipo genérico (definido pela linguagem) e MakeAtLeast um método genérico. As restrições disponíveis são muito semelhantes às restrições disponíveis em C #: qualquer tipo de valor, qualquer classe, uma classe ou interface específica e uma classe com um construtor sem parâmetros. Múltiplas restrições atuam como uma união aditiva.

Genericidade em Free Pascal

O Pascal livre implementou genéricos antes do Delphi, e com sintaxe e semântica diferentes. No entanto, desde a versão 2.6.0 do FPC, a sintaxe do estilo Delphi está disponível ao usar o modo de linguagem {$ mode Delphi}. Assim, os programadores de Free Pascal podem usar genéricos em qualquer estilo que preferirem.

Exemplo Delphi e Free Pascal:

// Delphi style
unit A;

{$ifdef fpc}
  {$mode delphi}
{$endif}

interface

type
  TGenericClass<T> = class
    function Foo(const AValue: T): T;
  end;

implementation

function TGenericClass<T>.Foo(const AValue: T): T;
begin
  Result := AValue + AValue;
end;

end.

// Free Pascal's ObjFPC style
unit B;

{$ifdef fpc}
  {$mode objfpc}
{$endif}

interface

type
  generic TGenericClass<T> = class
    function Foo(const AValue: T): T;
  end;

implementation

function TGenericClass.Foo(const AValue: T): T;
begin
  Result := AValue + AValue;
end;

end.

// example usage, Delphi style
program TestGenDelphi;

{$ifdef fpc}
  {$mode delphi}
{$endif}

uses
  A,B;

var
  GC1: A.TGenericClass<Integer>;
  GC2: B.TGenericClass<String>;
begin
  GC1 := A.TGenericClass<Integer>.Create;
  GC2 := B.TGenericClass<String>.Create;
  WriteLn(GC1.Foo(100)); // 200
  WriteLn(GC2.Foo('hello')); // hellohello
  GC1.Free;
  GC2.Free;
end.

// example usage, ObjFPC style
program TestGenDelphi;

{$ifdef fpc}
  {$mode objfpc}
{$endif}

uses
  A,B;

// required in ObjFPC
type
  TAGenericClassInt = specialize A.TGenericClass<Integer>;
  TBGenericClassString = specialize B.TGenericClass<String>;
var
  GC1: TAGenericClassInt;
  GC2: TBGenericClassString;
begin
  GC1 := TAGenericClassInt.Create;
  GC2 := TBGenericClassString.Create;
  WriteLn(GC1.Foo(100)); // 200
  WriteLn(GC2.Foo('hello')); // hellohello
  GC1.Free;
  GC2.Free;
end.

Linguagens funcionais

Genericidade em Haskell

O mecanismo de classe de tipo de Haskell oferece suporte à programação genérica. Seis das classes de tipo predefinidas em Haskell (incluindo Eqos tipos que podem ser comparados quanto à igualdade e Showos tipos cujos valores podem ser processados ​​como strings) têm a propriedade especial de oferecer suporte a instâncias derivadas. Isso significa que um programador que define um novo tipo pode afirmar que esse tipo deve ser uma instância de uma dessas classes de tipo especial, sem fornecer implementações dos métodos de classe, como geralmente é necessário ao declarar instâncias de classe. Todos os métodos necessários serão "derivados" - isto é, construídos automaticamente - com base na estrutura do tipo. Por exemplo, a seguinte declaração de um tipo de árvore binária afirma que deve ser uma instância das classes Eqe Show:

data BinTree a = Leaf a | Node (BinTree a) a (BinTree a)
      deriving (Eq, Show)

Isso resulta em uma função de igualdade ( ==) e uma função de representação de string ( show) definidas automaticamente para qualquer tipo de formulário, BinTree Tdesde que Tele próprio suporte essas operações.

O suporte para instâncias derivadas de Eqe Showfaz com que os seus métodos ==e showgenérico de um modo qualitativamente diferente de funções para-metricamente polimórfica: estes "funções" (mais precisamente, as famílias de tipo indexado de funções) pode ser aplicada aos valores de diversos tipos, e embora eles se comportam de maneira diferente para cada tipo de argumento, pouco trabalho é necessário para adicionar suporte para um novo tipo. Ralf Hinze (2004) mostrou que um efeito semelhante pode ser alcançado para classes de tipo definidas pelo usuário por certas técnicas de programação. Outros pesquisadores propuseram abordagens para este e outros tipos de genericidade no contexto de Haskell e extensões para Haskell (discutidas abaixo).

Pólipo

PolyP foi a primeira extensão de linguagem de programação genérica para Haskell . No PolyP, as funções genéricas são chamadas de politípicas . A linguagem apresenta uma construção especial na qual tais funções politípicas podem ser definidas por meio de indução estrutural sobre a estrutura do functor padrão de um tipo de dados regular. Os tipos de dados regulares no PolyP são um subconjunto dos tipos de dados Haskell. Um tipo de dados regular t deve ser do tipo * → * , e se a for o argumento de tipo formal na definição, então todas as chamadas recursivas para t devem ter a forma ta . Essas restrições excluem tipos de dados de tipo superior, bem como tipos de dados aninhados, onde as chamadas recursivas são de uma forma diferente. A função nivelar no PolyP é fornecida aqui como um exemplo:

   flatten :: Regular d => d a -> [a]
   flatten = cata fl

   polytypic fl :: f a [a] -> [a]
     case f of
       g+h -> either 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
Haskell genérico

Haskell genérico é outra extensão de Haskell , desenvolvido na Universidade de Utrecht, na Holanda . As extensões que ele fornece são:

  • Os valores indexados por tipo são definidos como um valor indexado sobre os vários construtores de tipo Haskell (unidade, tipos primitivos, somas, produtos e construtores de tipo definidos pelo usuário). Além disso, também podemos especificar o comportamento de valores indexados por tipo para um construtor específico usando casos de construtor e reutilizar uma definição genérica em outra usando casos padrão .

O valor indexado por tipo resultante pode ser especializado em qualquer tipo.

  • Tipos tipo indexados são tipos indexados sobre os tipos, definidos, dando um caso para tanto * e k → k' . As instâncias são obtidas aplicando o tipo indexado por tipo a um tipo.
  • As definições genéricas podem ser usadas aplicando-as a um tipo ou tipo. Isso é chamado de aplicativo genérico . O resultado é um tipo ou valor, dependendo de qual tipo de definição genérica é aplicada.
  • A abstração genérica permite que definições genéricas sejam definidas abstraindo um parâmetro de tipo (de um determinado tipo).
  • Tipos indexados por tipo são tipos que são indexados sobre os construtores de tipo. Eles podem ser usados ​​para fornecer tipos a valores genéricos mais envolvidos. Os tipos indexados por tipo resultantes podem ser especializados em qualquer tipo.

Por exemplo, a função de igualdade em Haskell genérico:

   type Eq {[ * ]} t1 t2 = t1 -> t2 -> Bool
   type Eq {[ k -> l ]} t1 t2 = forall u1 u2. Eq {[ k ]} u1 u2 -> Eq {[ l ]} (t1 u1) (t2 u2)

   eq {| t :: k |} :: Eq {[ k ]} t t
   eq {| Unit |} _ _ = True
   eq {| :+: |} eqA eqB (Inl a1) (Inl a2) = eqA a1 a2
   eq {| :+: |} eqA eqB (Inr b1) (Inr b2) = eqB b1 b2
   eq {| :+: |} eqA eqB _ _ = False
   eq {| :*: |} eqA eqB (a1 :*: b1) (a2 :*: b2) = eqA a1 a2 && eqB b1 b2
   eq {| Int |} = (==)
   eq {| Char |} = (==)
   eq {| Bool |} = (==)

Limpar

Clean oferece PolyP baseado em programação genérica e Haskell genérico conforme suportado pelo GHC> = 6.0. Ele parametriza por tipo como aqueles, mas oferece sobrecarga.

Outras línguas

As linguagens da família ML suportam programação genérica por meio de polimorfismo paramétrico e módulos genéricos chamados functores. Tanto o ML padrão quanto o OCaml fornecem functores, que são semelhantes aos modelos de classe e aos pacotes genéricos da Ada. Abstrações sintáticas de esquema também têm uma conexão com a genericidade - elas são na verdade um superconjunto de modelos C ++.

Um módulo Verilog pode assumir um ou mais parâmetros, aos quais seus valores reais são atribuídos na instanciação do módulo. Um exemplo é uma matriz de registro genérica em que a largura da matriz é fornecida por meio de um parâmetro. Tal array, combinado com um vetor de fio genérico, pode fazer um buffer genérico ou módulo de memória com uma largura de bit arbitrária a partir de uma implementação de módulo único.

VHDL , sendo derivado de Ada, também possui recursos genéricos.

C suporta "expressões genéricas de tipo" usando a palavra-chave: _Generic

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

Veja também

Referências

Fontes

Leitura adicional

links externos

C ++ / D
C # /. NET
Delphi / Object Pascal
Eiffel
Haskell
Java