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.