Published by Joao Morais on 10 Feb 2008 at 05:53 pm
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 Responses to “Construindo um interpretador orientado a objetos”
Leave a Reply
You must be logged in to post a comment.
Sanderson on 20 Feb 2008 at 9:49 am #
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.
Joao Morais on 20 Feb 2008 at 10:08 am #
Este interpretador será publicado no projeto RPNLib do SourceForge, você pode se inscrever na lista https://lists.sourceforge.net/mailman/listinfo/rpnlib-support-br para garantir que receberá notificação por email quando o projeto tornar-se ativo.
Osvaldo Medeiros on 04 May 2008 at 6:07 pm #
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.
Joao Morais on 04 May 2008 at 7:07 pm #
Ainda não. Verifique este outro componente: http://www.joaomorais.com.br/pascal/push.php?download=8944670 talvez sirva para o que você quer fazer.
gleury on 22 Jan 2009 at 4:05 pm #
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.
Joao Morais on 22 Jan 2009 at 4:38 pm #
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.
gleury on 02 Feb 2009 at 2:05 pm #
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
Joao Morais on 06 Feb 2009 at 8:07 am #
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.
Alexandre on 04 Mar 2009 at 3:38 pm #
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.
Reinaldo Schroeder on 23 Aug 2009 at 10:13 pm #
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