O que é metaprogramação? – metaprogramação paradigmas

Pergunta:


Algumas vezes ouvimos falar sobre “Metaprogramação”.

  • O que é?
  • Pra que serve?
  • Como e quando deve ser usada?
Autor da pergunta

Comunidade

A resposta do Guilherme Oderdenge demonstra bem o conceito de metaprogramação: “programas que criam programas”. Na minha resposta, vou dar uma abordagem mais prática (raramente alguém escreve um programa cuja saída é outro, exceto por diversão ou por se estar escrevendo um compilador…).

Muitas tarefas de programação são repetitivas. Embora a finalidade do computador seja nos ajudar com a repetição, na prática nem sempre isso é possível. Devido a limitações nas linguagens de programação (entre outras ferramentas computacionais), muitas vezes nos vemos escrevendo as mesmas coisas de novo e de novo e de novo. Se não dá para escrever diretamente um programa que evite essas repetições, o que resta é escrever um programa diferente e então alterá-lo através de uma rotina automatizada (um “meta-programa”).

De um modo geral, metaprogramação é toda programação que atua sobre outro programa, seja em formato fonte, binário, ou numa representação abstrata em memória.

Transformando o código-fonte

O exemplo mais conhecido é o #define do C. Ele permite substituir uma expressão complexa por outra mais simples, sendo que a substituição é feita pelo próprio compilador:

#define CURSOR(top, bottom) (((top) << 8) | (bottom))

Outro exemplo é o eval, suportado por diversas linguagens. Ele permite que um trecho de código seja formado usando-se não somente um texto estático mas também componentes dinâmicos:

eval("function(x) { alert(x + " + y + "); }"); // Ex.: function(x) { alert(x + 10); }

Transformando o código binário

O AspectJ é uma ferramenta que traz a Programação Orientada por Aspectos à linguagem Java. Um dos seus modos de uso é inserir código novo a classes existentes diretamente no código binário (bytecode-weaving). Escolhe-se um ou mais pontos bem definidos na execução de um programa, e escreve-se código para ser executado nesses pontos:

pointcut set() : execution(* set*(..) ) && this(Point);

after () : set() {
    Display.update();
}

O código acima executa Display.update(); após a execução de todo método da classe Point cujo nome comece com set.

Transformando uma representação abstrata

Toda linguagem que possui funções de primeira classe abre espaço para que as mesmas sejam modificadas programaticamente. A forma mais simples é através de decoradores:

function checarNulos(f) {
    return function() {
        for ( var i = 0 ; i < arguments.length ; i++ )
            if ( arguments[i] == null )
                throw 'Parâmetro nulo';
        return f.apply(this, arguments);
    }
}

var func = checarNulos(function(a, b) { return a + b; });

Nesse caso se está transformando uma função simples – sem nenhuma checagem de erro – em outra mais robusta (fail-fast). Isso é feito de forma genérica, podendo ser aplicada a diversos tipos de função, com diferentes quantidade e formatos dos parâmetros.

Outra possibilidade, se a linguagem possui “classes de primeira classe”, é aplicar transformações em uma classe como um todo (ex.: metaclasses).

Além desses casos, que eu particularmente chamo de “caixa preta” (pois somente a “fronteira” do código é alterado, sem o conhecimento do seu conteúdo), linguagens homoicônicas (como Lisp, Prolog ou XSLT) também permitem a transformação do código como um todo – incluindo instruções específicas, expressões, chegando até os símbolos terminais. Essas transformações são genericamente chamadas de “macros” (embora sejam bem mais flexíveis que as “macros” da família do C).

term_expansion((Cabeca :- Cauda), (Cabeca :- NovaCauda)) :-
    transforma(Cauda, NovaCauda).

transforma((A, B), (NA, NB)) :-
    transforma(A, NA),
    transforma(B, NB).

transforma(A is B + C, A is C + B).
transforma(X, X).

O código acima pega toda instrução “somar duas variáveis” e inverte a ordem dos operandos. Se há duas ou mais instruções separadas por E, ele transforma tanto a primeira quanto a segunda parte. Embora esse código execute “em tempo de compilação” (i.e. antes da definição da função ser incorporada ao programa), ele já atua sobre uma representação abstrata do código fonte, não sobre o texto em si (seria como a AST, mas em Prolog não há diferença entre “código” e “dados”, então a AST do programa é representada por estruturas de dados da própria linguagem).

Por fim, mesmo linguagens sem “X de primeira classe” ainda podem dar suporte a alguma forma de metaprogramação, na forma de reflexão (reflection). Ela consiste em se inspecionar a estrutura de seus dados e atuar nos mesmos de forma genérica:

void funcaoDemorada(int argumento, float argumento2, Object callback, String nomeMetodo) {
    ...
    Method m = callback.getClass().getMethod(nomeMetodo);
    m.invoke(callback);
}

Esse exemplo define uma função que pode chamar qualquer outra função [pública, zero parâmetros] em um objeto, bastando conhecer o nome dele. Sem isso, seria necessário que o objeto callback implementasse uma interface específica, e definisse um único método em conformidade com essa interface. Dessa forma, objetos de classes totalmente distintas e com métodos também distintos podem ser usados igualmente.

Nota: vários dos exemplos acima não podem ser considerados boa prática, e provavelmente existem melhores maneiras de se atingir o mesmo objetivo. Estão aí somente para exemplificar as técnicas mencionadas.

Concluindo, metaprogramação é muito mais amplo que “um programa gerar outro programa como saída”, se tratando de diversos casos em que usa-se um programa (ou trecho de um programa) para se manipular outro programa – transformá-lo em algo diferente daquilo que ele seria se fosse interpretado literalmente, tal como o programador o escreveu. É provável que alguém discorde em relação ao limite do que é “programação normal” (i.e. o simples uso das funcionalidades da própria linguagem) e o que é “meta-programação”, mas espero ter transmitido a ideia geral.

Imagine um homem que constrói carros.

Algumas vezes ele percebe que está sempre fazendo a mesma coisa. Então ele constrói fábricas para construir os seus carros, o que é muito mais produtivo. Ele está agora programando!

O tempo passa e, não obstante, mais uma vez, ele percebe que está sempre fazendo a mesma coisa de novo: construindo fábricas para construir os seus carros. Agora, então, ele decide construir fábricas que fabricam fábricas para construir carros. Isso é meta programação.

Metaprogramação é imensamente útil e poderosa, mas um problema na sua arquitetura pode fazer todas as vantagens se transformarem numa dor-de-cabeça monstruosa. Então, quando se aperfeiçoar, fique à vontade para usar… ou fique longe!

Resposta adaptada livremente do SOEN.

Uma analogia de poucas palavras

Metaprogramação é criar programas que criam programas.

E tecnicamente falando, como ficamos?

Metaprogramação consiste numa variedade de maneiras sobre como um programa consegue manipular à si mesmo: podemos utilizar macros, reflection, templates, etc e antes que você se pergunte qual é a melhor delas eu digo pra você esquecer. É meio clichê sim, mas isso vai depender não só do seu problema, como da sua linguagem, conhecimento e dominância – são um conjunto de fatores e mensurar qual é definitivamente a melhor, no chance.

Como e quando deve ser usada?

Vou matar dois coelhos numa cajadada só, também com uma tradução livre (fonte):

Erika é uma inteligente aluna de ciência da computação que está no primeiro ano. Ela já sabe várias linguagens de programação, incluindo C e Ruby. Durante a sua aula de introdução à programação, o professor Gomez, instrutor do curso, pegou ela conversando via chat em seu laptop. Como forma de punição, ele determinou que Erika escrevesse um programa em C que exibisse 1.000 linhas do seguinte texto:

1) Eu não vou conversar em aula.

2) Eu não vou conversar em aula.

[…]

999) Eu não vou conversar em aula.

1000) Eu não vou conversar em aula.

Um restrição adicional era que o programa não pudesse ter nenhum tipo de loop ou instruções “goto”. Ele deveria conter apenas uma grande função com 1.000 printfs. Algo assim:

#include <stdio.h>
int main(void) {
    printf("1) Eu não vou conversar em aula.n");
    printf("2) Eu não vou conversar em aula.n");

   /* 996 printfs omitidos. */

    printf("999) Eu não vou conversar em aula.n");
    printf("1000) Eu não vou conversar em aula.n");
    return 0;
}

Só que o professor Gomez era ingênuo, então ele basicamente esperou que Erika fosse escrever a instrução printf uma vez e copiasse para o clipboard para então colar 999 vezes para finalmente mudar os números. Ele acreditava que esse trabalho cansativo e remetitivo fosse o suficiente para ensinar à ela uma lição. Mas Erika imediatamente encontrou uma solução: metaprogramação. Ao invés de escrever esse programa na mão, por que não escrever outro programa que escreve esse programa automaticamente para ela? Então, ela escreveu o seguinte script em Ruby:

File.open('punishment.c', 'w') do |output|
    output.puts '#include <stdio.h>'
      output.puts 'int main(void) {'
      1.upto(1000) do |i|
          output.puts "    printf("#{i}. " +
          "Eu não vou conversar em aula.\n");"
      end

    output.puts '    return 0;'
    output.puts '}'
end

O código acima criou um arquivo chamado punishment.c que prevê 1.000+ linhas de código C.

Percebe a utilidade da metaprogramação? Como até o próprio artigo menciona, esse é um exemplo bem figurativo. Na vida real, vou usar o exemplo que o mesmo [artigo] deu, só que vou traduzir com as minhas palavras:

Você precisa incluir uma imagem PNG em um programa em C. Infelizmente, por alguma razão, o mecanismo de disponibilização desse programa aceita somente um arquivo: o executável. Então, de alguma maneira, nós precisamos incluir a informação da imagem PNG dentro desse arquivo. Para resolver isso, nós podemos fazer igualmente no exemplo anterior: ler a imagem PNG com um script Ruby e através do mesmo injetar no código em C.

Exemplo em código:

INPUT_FILE_NAME = 'ljlogo.png'
OUTPUT_FILE_NAME = 'ljlogo.h'
DATA_VARIABLE_NAME = 'ljlogo'


File.open(INPUT_FILE_NAME, 'r') do |input|
  File.open(OUTPUT_FILE_NAME, 'w') do |output|
    output.print "unsigned char #{DATA_VARIABLE_NAME}[] = {"
    data = input.read.unpack('C*')
    data.length.times do |i|
      if i % 8 == 0
        output.print "n    "
      end
      output.print '0x%02X' % data[i]
      output.print ', ' if i < data.length - 1
    end
    output.puts "n};"
  end
end

Basicamente, é isso.

Fonte

Related Posts:

Qual a diferença entre AppCompatActivity e Activity? – android android-activity
Pergunta: Qual a diferença da AppCompatActivity para Activity ? A partir de qual versão a AppCompatActivity foi adicionada ao Android? Autor da pergunta Luhhh A diferença reside ...
Como abreviar palavras em PHP? – php string
Pergunta: Possuo informações comuns como nome de pessoas e endereços, e preciso que elas contenham no máximo 30 caracteres sem cortar palavras. Exemplo: 'Avenida Natalino João Brescansin' ...
Qual é a finalidade de um parêntese vazio numa declaração Lambda? – c# expressões-lambda característica-linguagem
Pergunta: Criei um exemplo de uma declaração Lambda sem argumentos, entretanto, estou com duvidas referente a omissão do parêntese vazio () na declaração. Veja o exemplo: class ...
Boas práticas para URI em API RESTful – api rest restful
Pergunta: Estou com dúvida em relação às URIs de alguns recursos da api que estou desenvolvendo. Tenho os recursos projetos e atividades com relação 1-N, ...
Dúvidas sobre a integração do MySQL com Java – java mysql netbeans
Pergunta: Estou criando um sistema no NetBeans, utilizando a linguagem Java e o banco de dados MySQL. Escrevi o seguinte código para realizar a conexão ...
Qual é a finalidade da pasta Model do framework Inphinit? – php inphinit
Pergunta: No Inphinit micro-framework existe a pasta Model que fica dentro da pasta application, e nela é onde ficam as classes, mas eu estou muito ...
Uso do ‘@’ em variáveis – javascript typescript coffeescript
Pergunta: Vejo em algumas linguagens que compilam para javascript, como TypeScript e CoffeeScript, o uso do @ em variáveis, como também, casos em que o ...
Qual tamanho máximo um arquivo JSON pode ter? – json arquivo
Pergunta: Vou dar um exemplo para conseguir explicar minha duvida: Preciso recuperar informação de imagens vindas de uma API, esse banco de imagens me retorna JSON's ...
O que é Teste de Regressão? – terminologia engenharia-de-software testes
Pergunta: Na matéria de Teste de Software o professor abordou um termo chamado Teste de Regressão, isto dentro da disciplina de teste de software. Sendo ...
O que é um construtor da linguagem? – php característica-linguagem
Pergunta: Em PHP, já li e ouvi várias vezes a respeito dos Construtores da Linguagem. Os casos que sempre ouvi falar deles foi em casos ...
Função intrínseca para converter numérico para string – cobol
Pergunta: Estou a tentar saber se existe alguma função intrínseca do COBOL para converter um data numérico para string sem precisar usar a cláusula REDEFINES: ( ...
Porque usar implements? – java android
Pergunta: Qual a diferença entre usar btn.setOnClickListener(new OnClickListener() { e public class MainActivity extends Activity implements OnClickListener{ Estive fazendo um curso de Android e meu professor falou que ...
O que é XHTML e quando deve ser usado? – html xml xhtml
Pergunta: O que eu sei é que o XHTML precisa ser XML válido. Isso implica, por exemplo, que todas as tags precisam ser fechadas. Por ...
Uma placa aceleradora de vídeo pode melhorar o desempenho não-gráfico? [fechada] – desempenho
Pergunta: Para desenvolver em Ruby on Rails, eu utilizo aqui uma máquina virtual do VirtualBox com Ubuntu Server 14.04 sem interface gráfica instalada. Recentemente descobri uma ...
Concat() VS Union() – c# .net
Pergunta: Qual a diferença entre Concat() e Union() ? Quando usar Concat() e quando usar Union() ? Somente pode ser usado em list ? ...

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *