Проверка ввода с помощью конечных автоматов в Java

Проверка ввода с помощью конечных автоматов в Java

1. обзор

Если вы изучали CS, вы, несомненно, прошли курс компиляторов или что-то подобное; в этих классах изучается концепция конечного автомата (также известного как конечный автомат). Это способ формализации грамматических правил языков.

Вы можете узнать больше о темеhere иhere.

Так как же эта забытая концепция может быть полезна нам, программистам высокого уровня, которым не нужно беспокоиться о создании нового компилятора?

Что ж, получается, что концепция может упростить множество бизнес-сценариев и дать нам инструменты для размышлений о сложной логике.

В качестве быстрого примера, мы также можем проверить ввод без внешней сторонней библиотеки.

2. Алгоритм

В двух словах, такая машина объявляет состояния и способы перехода из одного состояния в другое. Если вы пропустите поток через него, вы можете проверить его формат с помощью следующего алгоритма (псевдокод):

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");
}

Мы говорим, что автомат «принимает» данный символ, если есть какая-либо стрелка, идущая из текущего состояния, в которой есть символ. Переключение состояний означает, что указатель следует, и текущее состояние заменяется на состояние, на которое указывает стрелка.

Наконец, когда цикл завершен, мы проверяем, может ли автомат «остановиться» (текущее состояние обозначено двумя кружками) и что вход был исчерпан.

3. Пример

Давайте напишем простой валидатор для объекта JSON, чтобы увидеть алгоритм в действии. Вот автомат, который принимает объект:

image

Обратите внимание, что значением может быть одно из следующих: строковое, целое, логическое, нулевое или другой объект JSON. Для краткости в нашем примере мы будем рассматривать только строки.

3.1. Код

Реализация конечного автомата довольно проста. У нас есть следующее:

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();
}

Отношения между ними:

  • Конечный автомат имеет один текущийState и сообщает нам, может он остановиться или нет (окончательное состояние или нет).

  • State имеет список переходов, за которыми можно следовать (исходящие стрелки)

  • Transition сообщает нам, принят ли символ, и дает нам следующийState

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();
    }
}

Обратите внимание, чтоFiniteStateMachine implementation is immutable. Это в основном так, что один его экземпляр можно использовать несколько раз.

Далее у нас есть реализацияRtState. Методwith(Transition) возвращает экземпляр после добавления перехода для удобства использования. State также сообщает нам, окончательно ли оно (обведено двойным кружком) или нет.

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;
    }
}

И, наконец,RtTransition, который проверяет правило перехода и может выдать следующийState:

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
}

Код вышеhere. С помощью этой реализации вы сможете создать любой конечный автомат. Алгоритм, описанный в начале, так же прост, как:

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());

Проверьте тестовый классRtFiniteStateMachineTest, чтобы увидеть методbuildJsonStateMachine(). Обратите внимание, что он добавляет еще несколько состояний, чем изображение выше, чтобы также ловить кавычки, которые правильно окружают строки.

4. Заключение

Конечные автоматы - это отличные инструменты, которые вы можете использовать для проверки структурированных данных.

Однако они малоизвестны, поскольку могут усложняться, когда дело доходит до сложного ввода (поскольку переход может использоваться только для одного символа). Тем не менее, они хороши, когда дело доходит до проверки простого набора правил.

Наконец, если вы хотите выполнить более сложную работу с использованием конечных автоматов, стоит изучить две библиотекиStatefulJ иsquirrel.

Вы можете проверить образцы кодаon GitHub.