Construindo um interpretador orientado a objetos

Outro dia precisei colocar fórmula em um dos sistemas que desenvolvo, para que o usuário tenha mais liberdade para informar como um custo deva ser calculado. É permitido que ele faça algo como 0.012 * 66 * 96 * Chapas / FormatoImpressao, aonde as variáveis apresentadas são atributos do objeto de negócio.

Nada tão sério, existem alguns interpretadores de fórmula matemática por aí, no entanto dois motivos me fizeram implementar meu próprio interpretador: primeiro a falta de integração com meu framework, a menos que o parser fosse customizável suficiente para que eu construisse outro interpretador em cima dele para a interpretação dos meus atributos; segundo que eu perderia a oportunidade impar de construir outro parser, e ainda mais um para interpretação de fórmula matemática. A seguir conto um pouco sobre ele e como se constroi um interpretador.

Escrever o PressObjects fez com que eu precisasse pegar o espírito da coisa para construção de interpretadores. O projeto tem vários, como um interpretador Pascal para alteração automatizada dos fontes do projeto, um interpretador de arquivo de configuração para permitir a configuração de serviços (tal como um conector para banco de dados, entre vários outros) sem a necessidade de alterar o binário do projeto, um interpretador de metadata (parecido com DDL de bancos de dados) para criar classes de negócio com informações de runtime, e um interpretador de OQL (object query language) que transforma uma consulta orientada a objetos para uma SQL que bancos relacionais possam entender.

O interpretador de fórmula não seria tão diferente destes outros, utilizaria as mesmas classes base e precisaria meramente entender uma gramática diferente: uma fórmula matemática. Este interpretador já está disponível na versão atual do PressObjects (download aqui)

Os interpretadores que construi foram baseados no padrão de projeto Interpreter (do GoF), com algumas adaptações que eu julguei pertinente. Na minha variante, um interpretador está dividido em duas partes: o reader e o parser.

O reader

O reader é a parte do interpretador que desmonta a entrada em partes menores, os tokens. Para a fórmula 22+-5*AB os tokens são 22, +, -5, * e AB. Essa parte parece moleza, mas cuidado com o julgamento precipitado. São vários os tipos de entrada que o interpretador precisa reconhecer, ele precisa entender quando um token termina, precisa reconhecer sinais iguais com significados diferentes, ex, aquele menos logo acima é um sinal, e não um operador. E por último mas não menos importante, precisa ter vários métodos para que tenha um uso tão simplificado quanto possível pelo parser.

O bom do reader é que, uma vez construido, ele será o mesmo para a esmagadora maioria de interpretadores, com alguma pequena customização como o token “>=”. Algumas gramáticas interpretam esta sequência como dois tokens, outras o lêem inteiro como um único token de dois caracteres.

O parser

Concluido o reader é hora de construir o parser. O parser é um conjunto de classes utilizadas para dar vida à gramática, e são dois os métodos mais importantes de cada uma de suas classes: apply e read.

O apply tem a função de validar a próxima entrada do reader, se a entrada bater com o que esta classe do parser está esperando, retorna verdadeiro. Exemplo:

class function TAddOperation.Apply(Reader: TParserReader): Boolean;
begin
  Result := Reader.ReadToken = '+';
end;

Simples assim. Se o próximo token para o qual o reader aponta for um sinal de mais, então a classe que interpreta o sinal de mais pode receber aquela entrada.

Já o read tem a função de ler e interpretar a entrada atual, devidamente apontada pelo reader. Para um exemplo mais simples, vou criar uma classe completa para ler o corpo de um select bem simplificado:

class function TSelect.Apply(Reader: TParserReader): Boolean;
begin
  Result := SameText(Reader.ReadToken, 'select');
end;

procedure TSelect.Read(Reader: TParserReader);
begin
  Reader.ReadMatchText('select');
  FFields := Parse(Reader, [TSelectFields], 'campo(s) da tabela');
  Reader.ReadMatchText('from');
  FTableName := Reader.ReadIdentifier;
  FWhere := Parse(Reader, [TSelectWhere]);
  FOrderBy := Parse(Reader, [TSelectOrderBy]);
end;

ReadMatchText é um método do reader que verifica se a próxima entrada é o que está no parâmetro. Se não for ele ergue uma exception e nem deixa o processamento seguir adiante. ReadIdentifier lê o próximo token e ergue uma exception se o token não for um nome de identificador válido. Parse é um método do próprio parser que recebe uma lista de classes e verifica se alguma delas retorna True ao chamar o respectivo Apply. A primeira que retornar é a que interpretará a entrada, se nenhuma retornar ele erguerá uma exception caso o último argumento tenha o nome do que ele estava esperando. Ele também retorna o objeto que interpretou a entrada e deixa o reader no ponto para o próximo.

Para que o parser chegue ao seu objetivo, que é entregar informação mastigada, basta construir novos métodos que leiam as informações a partir dos objetos que foram criados. Estes objetos, no exemplo acima, são apontados pelo FFields, FTable, FWhere, FOrderBy. No caso do interpretador de fórmula, ele converte a entrada em uma lista de operações que será executada quando o usuário pedir o resultado da fórmula.

Que tal o truque? Não que um interpretador seja um primor de simplicidade, mas ele torna muito mais simples a construção de gramáticas, e diga-se de passagem com uma grande ajuda da programação orientada a objetos.

10 thoughts on “Construindo um interpretador orientado a objetos

  1. Olá João, há tempo venho acompanhando as suas publicaçoês na web, elas tem sido de grande ajuda, há tempo que veio procurando um interpretador de formulas, como esse que você descreveu, assim que tiver concluido entra em contato comigo, tenho interesse em usá-lo. Trabalho com desenvolvimento de software, preciso colocar um interpretor de formula no sistema, para que o usuário final, possa fazer as alteraçoes que julgarem necessária. Como trabalho com vários clientes tenho que ficar criando parâmetros para diferentes tipos de cliente, faço isso dentro do exe, toda alteração preciso mandar uma nova versão para o cliente, com o interpretador de formula acredito que vou minimizar esse tipo de situação.

  2. Caro João, conhecí hoje a sua publicação sobre o interpretador matemático. Estou desenvolvendo para custo industrial de produtos eterogêneos, com fórmulas diversas e deparei com este problema: como fazer o sistema reconhecer uma expressão matemática e interpretá-la? Estou me inscrevendo na lista para ter acesso ao seu projeto. Como já estamos em Maio, pode me dizer se ele já foi publicado? Obrigado.

  3. não sei se o q vou perguntar tem muito a ver com o contexto: Mais como eu posso fazer um compilador de *.*bat em delphi? e vc bem q podia criar um super tutorial ensinado isto.

  4. A sua questão não ficou muito clara. Para criar um interpretador, o que eu posso sugerir a você é ler este artigo para pegar uma idéia para a implementação, e colocar suas dúvidas em listas de discussão por email ou em fóruns.

    Quanto a outros tutoriais, não creio que posso ajudar. Este artigo chegou ao meu limite em conhecimento, e especialmente em didática.

  5. Como posso criar um compilador de linguagens (caso .bat) ou um interpretador em delphi.
    no estilo edito, onde o usuario irar digitar comandos do dos, e apos compilar irar gerar um .exe

    por favor responda por e-mail gleuryhwa@hotmail.com

  6. Escrever um compilador e um gerador de código está um pouco além das minhas humildes habilidades. Isto é assunto para livros, tanto de arquitetura de processador como técnicas de parsing. Recomendo você procurar literatura a esse respeito e pegar projetos que já fazem isto, como o Free Pascal, a fim de se familiarizar com o assunto.

  7. Estou analisando alguns interpretadores, mas estou me deparando com alguns probelmas.
    Estou construindo um sistema onde o usuario informa as formulas ( rotinas em pascal ) que deseja realizar. Porem, o sistema acessa a banco de dados, e tenho o objeto tabela aberto no inicio da rotina ( Data module ). Alguem conhece algum interpretador de rotinas como em pascal que eu consiga fazer isto ?

    Exemplo :

    DataModule ( qFunc ) Query com tabela aberta.

    script de processamento ( Formula armazenada no banco )

    formulas.pas

    DataModule.qFunc.first ;
    while not qFunc.eof do
    begin

    calculos….

    end ;

    result := ( calculos processados )

    Um detalhe, que tenho que aproveitar o Data Module da aplicacao em Delphi.

    Eu encontrei o DWS(http://www.forumweb.com.br/foruns/lofiversion/index.php/t62413.html) , porem, eu nao consegui fazer o que gostaria, pois dentro dele, sou obrigado a abrir uma nova conexao, e neste caso, nao posso, tenho que aproveitar o meu datamodule.

    Alguem pode me ajudar ?
    Muito obrigado.

  8. Prezado João.

    Através das informações obtida em seu blog, consegui construir algo havia muito não encontrava meios: um programa que pudessse interpretar uma série de equações matemáticas simples e intercalá-las em série.

    Gostaria muito de agradecer esta disponibilidade e como retribuição, estou disponibilizando o programa com todo o fonte e documentaçaõ em meu site, para que outros possam utilizá-lo como exemplo.

    a interface ainda está precária, mas pretendo melhorá-la em breve.

    Obrigado
    Reinaldo

Comments are closed.