Validando entrada com autômatos finitos em Java

Validando entrada com autômatos finitos em Java

1. Visão geral

Se você estudou ciência da computação, sem dúvida fez um curso sobre compiladores ou algo semelhante; nessas aulas, o conceito de Autômato Finito (também conhecido como Máquina de Estados Finitos) é ensinado. Essa é uma maneira de formalizar as regras gramaticais das línguas.

Você pode ler mais sobre o assuntohere ehere.

Então, como esse conceito esquecido pode ser útil para nós, programadores de alto nível, que não precisam se preocupar em construir um novo compilador?

Bem, acontece que o conceito pode simplificar muitos cenários de negócios e nos fornecer as ferramentas para raciocinar sobre lógica complexa.

Como um exemplo rápido, também podemos validar a entrada sem uma biblioteca externa de terceiros.

2. O Algoritmo

Em poucas palavras, essa máquina declara estados e maneiras de passar de um estado para outro. Se você colocar um fluxo nele, poderá validar seu formato com o seguinte algoritmo (pseudocódigo):

for (char c in input) {
    if (automaton.accepts(c)) {
        automaton.switchState(c);
        input.pop(c);
    } else {
        break;
    }
}
if (automaton.canStop() && input.isEmpty()) {
    print("Valid");
} else {
    print("Invalid");
}

Dizemos que o autômato “aceita” o caractere fornecido se houver alguma seta saindo do estado atual, que possui o caractere. Alternar estados significa que um ponteiro é seguido e o estado atual é substituído pelo estado para o qual a seta aponta.

Finalmente, quando o loop termina, verificamos se o autômato “pode parar” (o estado atual é de um círculo duplo) e se a entrada foi esgotada.

3. Um exemplo

Vamos escrever um validador simples para um objeto JSON, para ver o algoritmo em ação. Aqui está o autômato que aceita um objeto:

image

Observe que o valor pode ser um dos seguintes: string, número inteiro, booleano, nulo ou outro objeto JSON. Para fins de brevidade, em nosso exemplo, consideraremos apenas strings.

3.1. O código

A implementação de uma máquina de estados finitos é bastante direta. Temos o seguinte:

public interface FiniteStateMachine {
    FiniteStateMachine switchState(CharSequence c);
    boolean canStop();
}

interface State {
    State with(Transition tr);
    State transit(CharSequence c);
    boolean isFinal();
}

interface Transition {
    boolean isPossible(CharSequence c);
    State state();
}

As relações entre eles são:

  • A máquina de estado tem umState atual e nos diz se pode parar ou não (se o estado é final ou não)

  • AState tem uma lista de transições que podem ser seguidas (setas de saída)

  • UmTransition nos diz se o caractere é aceito e nos dá o próximoState

publi class RtFiniteStateMachine implements FiniteStateMachine {

    private State current;

    public RtFiniteStateMachine(State initial) {
        this.current = initial;
    }

    public FiniteStateMachine switchState(CharSequence c) {
        return new RtFiniteStateMachine(this.current.transit(c));
    }

    public boolean canStop() {
        return this.current.isFinal();
    }
}

Observe queFiniteStateMachine implementation is immutable. Isso ocorre principalmente para que uma única instância possa ser usada várias vezes.

A seguir, temos a implementaçãoRtState. O métodowith(Transition) retorna a instância após a transição ser adicionada, para fluência. UmState também nos diz se é final (duplo círculo) ou não.

public class RtState implements State {

    private List transitions;
    private boolean isFinal;

    public RtState() {
        this(false);
    }

    public RtState(boolean isFinal) {
        this.transitions = new ArrayList<>();
        this.isFinal = isFinal;
    }

    public State transit(CharSequence c) {
        return transitions
          .stream()
          .filter(t -> t.isPossible(c))
          .map(Transition::state)
          .findAny()
          .orElseThrow(() -> new IllegalArgumentException("Input not accepted: " + c));
    }

    public boolean isFinal() {
        return this.isFinal;
    }

    @Override
    public State with(Transition tr) {
        this.transitions.add(tr);
        return this;
    }
}

E, finalmente,RtTransition que verifica a regra de transição e pode dar o próximoState:

public class RtTransition implements Transition {

    private String rule;
    private State next;
    public State state() {
        return this.next;
    }

    public boolean isPossible(CharSequence c) {
        return this.rule.equalsIgnoreCase(String.valueOf(c));
    }

    // standard constructors
}

O código acima éhere. Com essa implementação, você poderá construir qualquer máquina de estado. O algoritmo descrito no início é tão direto quanto:

String json = "{\"key\":\"value\"}";
FiniteStateMachine machine = this.buildJsonStateMachine();
for (int i = 0; i < json.length(); i++) {
    machine = machine.switchState(String.valueOf(json.charAt(i)));
}

assertTrue(machine.canStop());

Verifique a classe de testeRtFiniteStateMachineTest para ver o métodobuildJsonStateMachine(). Observe que ele adiciona mais alguns estados do que a imagem acima, para também capturar as aspas que circundam as Strings corretamente.

4. Conclusão

Autômatos finitos são ótimas ferramentas que você pode usar na validação de dados estruturados.

No entanto, eles não são amplamente conhecidos porque podem ficar complicados quando se trata de entrada complexa (uma vez que uma transição pode ser usada para apenas um caractere). No entanto, eles são ótimos quando se trata de verificar um conjunto simples de regras.

Finalmente, se você quiser fazer um trabalho mais complicado usando máquinas de estado finito,StatefulJesquirrel são duas bibliotecas que vale a pena examinar.

Você pode verificar as amostras de códigoon GitHub.