Оказывается, конечные автоматы имеют много общего с редукторами, а редукторы являются важным элементом при управлении неизменяемыми хранилищами данных.

В моей прошлой статье было рассмотрено, как конечные автоматы упрощают код. В итоге мы расширили классический конечный автомат, добавив к переходам аннотации действий, которые выполняются при переходе.

В этом примере, если приложение простаивает и игрок присоединяется, мы переходим в новое состояние Wait for P2. Попутно мы запускаем appP1 действие, записывая ID этого нового игрока.

Назовем данные, которые приложение поддерживает для конкретной игры, ее контекстом. (Обычно мы называем это состоянием, но здесь это означает нечто иное.)

В этой статье у нас был контекст обновления действий, который хранился в переменных модуля. Ясно, что это не лучшая идея для любого настоящего приложения. Итак, давайте изменим код нашего конечного автомата. Когда мы запускаем новый конечный автомат, мы передаем ему новый начальный контекст. Каждый раз, когда конечный автомат вызывает действие, он передает ему как событие, вызвавшее действие, так и текущий контекст. Возвращаемое значение из действия становится новым значением контекста.

Вот набросок кода, реализующего конечный автомат входа в игру с использованием этой техники:

В этом коде мы реализовали конечный автомат как класс. Это позволяет запоминать контекст между вызовами. Когда ему передается событие, он ищет следующее состояние и обработчик действия. Он вызывает действие, передавая ему как текущий контекст, так и событие. Значение, возвращаемое действием, становится следующим контекстом.

Чего мы здесь достигли? Путем передачи контекста через код действия мы добавили еще один уровень разделения: теперь состояние приложения отделено от кода приложения. Это значит, что мы можем лечить его самостоятельно. Возможно, контекст действительно большой, а события происходят нечасто. В этих обстоятельствах конечный автомат может записать контекст в какое-то внешнее хранилище, заменив его тривиальным прокси-объектом и освободив память. Когда наступает событие, оно может восстановить контекст. Код приложения не обращает внимания на эту реализацию.

Действие - это редуктор

Давайте сделаем шаг назад.

Скажем, нам нужно суммировать элементы в массиве. Мы могли бы просто использовать цикл, но крутые ребята (вроде вас), вероятно, использовали бы библиотечную функцию с именем reduce (или, возможно, foldl). На JavaScript вы бы написали

const sum = numbers.reduce((sum, nextValue) => sum + nextValue, 0)

На функциональном языке, таком как PureScript, это может быть

sum = foldl (+) 0 numbers

Эти два фрагмента кода выглядят совершенно по-разному, но их действие идентично. Ноль в каждом вызове - это начальное значение для суммы: перед тем, как мы начнем, сумма равна нулю.

Вызов reduce или foldl затем проходит по списку номеров. Для каждого числа в списке он обновляет сумму, вызывая предоставленную вами функцию, передавая этой функции текущую сумму и следующее число. Функция возвращает новое значение суммы. Это новое значение затем передается вместе со следующим числом в списке и так далее, пока список не будет исчерпан. reduce (или foldl) затем возвращает окончательное значение этой суммы.

В общем случае функция, переданная в сокращение, может применять любую двоичную операцию, которая имеет смысл между начальным значением и элементами в списке.

const max = numbers.reduce((max, next) => Math.max(max, next))
const length = numbers.reduce((len, _) => len + 1, 0)
const reversed = 
    numbers.reduce((result, next) => result.unshift(next), [])

Функция, которую вы передаете reduce, называется (подождите…) reducer. Редуктором может быть любая функция, которая принимает два параметра и имеет сигнатуру типа

reducer(accumulator: typeA, nextValue: typeB): typeA

То есть он обновляет значение типа A, применяя следующее значение типа B, возвращая новое значение типа A.

Теперь давайте посмотрим на действие в нашем конечном автомате:

function playerOneLeaves(appContext, _ev) {
  return { ...appContext, player1: null, player2: null }
}

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

Редукторы - это большое дело

Разработчики начинают принимать идеи, которыми функциональные программисты дорожили на протяжении 50 лет. Ключевым примером является концепция неизменяемых данных. В этой модели существующие данные никогда не изменялись; вместо этого создается обновленная копия. Для этого мы используем редукторы: берем старые данные и информацию о необходимых изменениях и возвращаем новую, обновленную копию данных.

X Это основная идея фреймворка Redux, таких баз данных, как Datomic, и модели обработки Erlang и Elixir.

Редукторы в конечном автомате - более серьезное дело

В большей части кода, который я видел, в котором используются редукторы, эти редукторы представляют собой изолированные фрагменты кода. Иногда они связаны вместе, но эта цепочка выполняется тем же кодом, который реализует редукторы. Происходит некоторая связь.

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

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

Есть и второстепенный выигрыш, когда речь идет о мониторинге вашего приложения. Все действия инициируются из центрального места: государственной машины. Это дает нам единое место для ведения журнала, измерения производительности, A / B-тестирования и т. Д.

Идея, исходящая из базовой механики, оказывается фундаментальным фактором разработки приложений. Круто, а?

Возможное будущее содержание

Весь этот бизнес можно продвинуть на шаг вперед, если у нас есть сам конечный автомат, управляемый содержимым контекста приложения. Таким образом, отдельные действия / редукторы могут просто обновлять контекст, а конечный автомат решает, что делать дальше. И, в соответствии с исторической предвзятостью, лежащей в основе этой серии статей, управление государствами контекстом - это, конечно, старая идея. Это было сделано в программном обеспечении еще в 1950-х годах с помощью техники, называемой таблицами решений. Дайте мне знать, если вы хотите увидеть статью об этом.

Примечания:

  1. Конечно, скорее всего, не все так просто. Изменение наших требований может затронуть как конечный автомат, так и группу редукторов. Но у нас по-прежнему есть все преимущества разделения ответственности.