Saltar para o conteúdo

Programação genérica

Origem: Wikipédia, a enciclopédia livre.

Programação genérica é um paradigma de programação no qual os algoritmos são escritos em uma gramática estendida de forma a adaptar-se através da especificação das partes variáveis que são definidas na instância do algoritmo. Especificamente, a gramática estendida eleva um elemento não variável ou uma construção implícita na gramática base para uma variável ou constante, permitindo a utilização do código genérico.

É diferente da forma normal de programação na medida em que invoca de certa forma as facilidades de metaprogramação da linguagem. Como isso ocorre em uma extensão da linguagem, novas semânticas são introduzidas e a linguagem é enriquecida no processo. É relacionada com a metaprogramação, mas não envolve a geração de código fonte, pelo menos visivelmente ao programador. É diferente também da programação por macros, já que esta refere-se somente a busca e substituição de termos, não fazendo parte da gramática da linguagem, implementada somente na fase de pré-processamento do código.

Para efeitos práticos, o paradigma permite que um parâmetro assuma diferentes tipos de dados desde que certas regras sejam mantidas, como sub-tipos e assinaturas. Por exemplo, para criar uma lista usando programação genérica, uma possível declaração seria List<T>, no qual T é o tipo de dado. Para instanciar, poderia-se usar List<Inteiro> ou List<Animal>, já que o conceito de lista independe do tipo utilizado.

Entre linguagens orientadas a objeto, C++, Linguagem D, BETA, Eiffel e versões de Java (1.5 e acima) fornecem o paradigma genérico. Visual Basic .NET, C# e Delphi.Net começaram a fornecer o paradigma a partir do .NET 2.0. Muito antes de todas as linguagens mencionadas, programação genérica já havia sido implementada na década de 1970 em linguagens como CLU e Ada.

Mas somente o conceito de templates do C++ que popularizou o conceito. A técnica permite que algoritmos sejam escritos independente dos tipos de dados utilizados.

Os autores do conceituado livro de 1995 Design Patterns[1] referem-se à programação genérica como tipos parametrizados, o que permite que um tipo possa ser definido sem especificar todos os outros tipos que ele utilizada. Os autores ainda descrevem que tal técnica é muito poderosa, especialmente quando combinada com o padrão Delegar.

Utilização em C++: templates

[editar | editar código-fonte]

Templates são de grande utilidade para programadores C++, especialmente quando combinado com herança múltipla e sobrecarga de operadores. A biblioteca padrão do C++ fornece várias funções úteis dentro de uma rede de templates conectados.

Templates em C++ podem também serem usados para funções diversas à programação genérica. Um exemplo é a meta programação por templates, um método para executar algoritmos em tempo de compilação ao invés de execução.

Existem dois tipos de templates. Uma função template se comporta como uma função que pode aceitar argumentos de vários tipos diferentes. Por exemplo, a biblioteca STL do C++ contém funções template max(x, y) que retornam ou x ou y, qual for maior. max() poderia ser definida assim:

template <typename T>
T max(T x, T y)
{
    if (x < y)
        return y;
    else
        return x;
}

Esse template pode ser chamado para diversos tipos de dado:

cout << max(3, 7);           // imprime 7 -> tipo de dado é ''int''
cout << max(3.239, 5.238);   // imprime 5.238 -> tipo de dado é ''float''

O compilador determina, ao examinar os argumentos, que a primeira chamada possui assinatura max(int, int) e instancia uma versão da função no qual o tipo T é int. Da mesma forma, ele determina que a segunda chamada possui assinatura max(float, float), e instancia a função para float.

Isso somente funciona pois os tipos int e float definem a operação <. Para tipos de dados próprios (como classes), é possível usar sobrecarga de operadores para definir < para seu tipo, permitindo que ele seja usado na função max(). Apesar de parecer um benefício simples, no contexto de bibliotecas como a STL isso permite que programadores tenham mais funcionabilidade para seus tipos definidos ao definir os operadores. Por exemplo, definir < permite que um tipo possa ser usado com as funções padrão sort(), stable_sort(), e binary_search(); além de estruturas de dados como sets, entre outras.

Como exemplo contrário, o tipo padrão complex não define o operador <, pois não existe ordenação definida em números complexos. Logo max(x, y) não poderá ser utilizado se x e y são do tipo complex. Da mesma forma, outros templates que necessitam de < não podem ser utilizados com complex.

Uma classe template amplia o mesmo conceito para classes. Elas são usadas geralmente para criar containers genéricos. Por exemplo, a STL possui o container list, que representa uma lista encadeada. Para criar listas encadeadas de inteiros, utiliza-se list<int>. Da mesma forma, uma lista de cadeias de texto é definida como list<string>. list possui um conjunto de funções padrão associadas a ele, que funcionam independente do tipo utilizado para a lista.

Vantagens e desvantagens

[editar | editar código-fonte]

Algumas aplicações de templates, como a função max(), eram anteriormente criadas por macros de função:

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

Tanto macros quanto templates são instanciados em tempo de compilação. Macros são sempre expandidas no próprio local onde foram utilizadas; templates também podem ser expandidos no próprio local onde foram utilizadas, se o compilador considerar adequado. Logo, tanto macros de função quanto funções template não exigem mais processamento na execução.

Apesar disso, templates são considerados uma evolução sobre macros nesse propósito. Eles verificam a tipagem de dados, evitam alguns dos erros mais comuns ao se utilizar macros excessivamente e são mais robustos que macros.

Antigamente, os compiladores não tinham bom suporte para templates. No entanto, praticamente todos os compiladores atuais do mercado conseguem lidar com templates sem problemas. Ainda assim, existem duas desvantagens no uso de templates. Primeiro, quase todos os compiladores produzem mensagens de erros em templates que são confusas e de pouca ajuda, o que torna o desenvolvimento mais difícil. Segundo, quanto mais tipos de dados diferentes utilizados em templates, geralmente mais código é gerado pelo compilador (uma versão da função ou classe para cada tipo de dado), logo, o uso indiscriminado pode levar a executáveis excessivamente grandes.

O primeiro problema mencionado será eliminado com a chegada dos Conceitos novo padrão C++ (C++0x, que está previsto para 2009). Através dos conceitos será possível estabelecer um mecanismo formal para verificação de tipos e operações esperadas por um parâmetro template. Com isso, mensagens de erro serão sempre claras e objetivas.

Utilização em Haskell

[editar | editar código-fonte]

Em Haskell, algumas extensões à linguagem foram desenvolvidas para a programação genérica. Além disso a própria linguagem contém alguns aspectos do paradigma incluídos.

Suporte na própria linguagem

[editar | editar código-fonte]

Dado a declaração de um tipo de dado definido pelo usuário (uma árvore binária com o tipo de dado a para os nós):

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

A palavra-chave deriving seguida de dois tipos de dados torna possível para o programador ter uma função definida para BinTree de igualdade (Eq) e saída padrão (Show). Assim, o compilador Haskell pode gerar instâncias de funções particulares para qualquer tipo de dado (com algumas restrições).

Extensão PolyP

[editar | editar código-fonte]

PolyP foi a primeira linguagem de extensão para programação genérica para Haskell. Nelas as funções são chamadas polytypic. A linguagem introduz uma construção especial no qual as funções polytypic podem ser definidas por induções estruturais sobre a estrutura do functor do padrão de um tipo de dado. Tipos de dado regulares (nativos) são um sub-conjunto dos tipos regulares do Haskell. Um exemplo é dado abaixo:

    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

Utilização em OCaml

[editar | editar código-fonte]

Em OCaml, a programação genérica é implementada através de polimorfismo. Apesar da linguagem ter tipagem estática, os tipos não precisam ser declarados (a não ser no caso em que se pretende restringir a entrada ou saída de uma função), então sempre que possível os códigos escritos irão se aplicar a vários tipos de dados. Como um exemplo, a função:

 let duplicar lista =
   lista @ lista

Irá duplicar a lista de entrada, ou seja, para uma lista que na sintaxe de OCaml seja escrita como [1; 2; 3], essa função irá retornar [1; 2; 3; 1; 2; 3]. Essa função tem o mesmo comportamento para lista de outros tipos de dados, como char e float.

Esse tipo indefinido é chamado de tipo polimórfico. OCaml representa o polimorfismo por letra, de forma que a função acima irá resultar na definição:

 val duplicar : 'a list -> 'a list = <fun>

Indicando que a lista de entrada e a lista de saída são de um tipo indefinido (mas que são do mesmo tipo; caso fossem de tipos diferentes, uma seria 'a list, a outra 'b list).

Referências

  1. Erich Gamma et. al. (1994). Design Patterns. Elements of Reusable Object-Oriented Software. [S.l.]: Addison-Wesley Professional. 416 páginas. ISBN 978-0201633610 

Leitura adicional

[editar | editar código-fonte]

Ligações externas

[editar | editar código-fonte]