Неизменяемые данные в асинхронных системах

У меня есть хорошее представление о преимуществах использования неизменяемых данных в моих приложениях, и я довольно доволен идеей использования этих неизменяемых структур в простой синхронной среде программирования.

Вот хороший пример где-то в Stack Overflow, который описывает состояние управления игрой, передавая состояние в последовательности рекурсивных вызовов, примерно так:

function update(state) {
  sleep(100)

  return update({
    ticks: state.ticks + 1,
    player: player
  })
}

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

Кажется довольно легко перевести это на простую асинхронную модель, например на Javascript.

function update(state) {
  const newState = {
    player,
    ticks: state.ticks + 1
  };

  setTimeout(update.bind(this, newState), 100);
}

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

Если мы добавим событие click в пример, мы получим код, который выглядит следующим образом.

window.addEventListener('click', function() {
  // I have no idea what the state is
  // because only our update loop knows about it
});

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

window.addEventListener('click', function() {
  const state = getState();

  createState({
    player,
    clicks: clicks + 1
  });
});

Но, похоже, для этого требуется какой-то изменчивый менеджер состояний?

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

window.addEventListener('click', function() {
  createAction('click', e);
});

function update(state, actions) {
  const newState = {
    player,
    ticks: state.ticks + 1,
    clicks: state.clicks + actions.clicks.length
  };

  setTimeout(update.bind(this, newState, []), 100);
}

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

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

Ответы

Ответ 1

Вам может быть интересно взглянуть на Redux. Redux использует аналогичный подход:

  • Он моделирует все состояние приложения как один неизменяемый объект.
  • Действия пользователя - это, по существу, сообщения, отправленные в store для произвольной обработки.
  • Действия обрабатываются редукторными функциями в форме f(previousState, action) => newState. Это более функциональный подход, чем ваша оригинальная версия.
  • store запускает редукторы и поддерживает одно неизменное состояние приложения.

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

В дополнение к действиям пользовательского интерфейса вы также можете выполнить действие tick, которое срабатывает в цикле - это просто другое событие ввода, обработанное одним и тем же набором редукторов.

Ответ 2

В попытке напрямую ответить на ваш вопрос:

"Как выглядит дизайн для системы, когда есть несколько асинхронных источников событий, и мы хотим, чтобы все было неизменным? Или, по крайней мере, какой хороший шаблон для управления изменчивостью в такой системе?"

Правильный шаблон решения для этого проекта в мире unix был асинхронным очередями сообщений FIFO (AMQ), начиная с System 5, в то время как теоретически существуют условия, при которых на практике могут возникать условия гонки и неопределенность состояния почти никогда не бывает. На самом деле, ранние исследования надежности AMQ определили, что эти ошибки возникли не из-за задержки передачи, а из-за столкновения с синхронными запросами прерывания, поскольку ранние AMQ были по существу просто каналами, реализованными в пространстве ядра. Современное решение, фактически решение Scala, заключается в реализации AMQ в общей защищенной памяти, тем самым устраняя медленные и потенциально опасные вызовы ядра.

Как оказалось, если ваша общая пропускная способность сообщения меньше общей пропускной способности канала, а ваше расстояние передачи меньше, чем светлая секунда - сопротивление/переключение, ваша вероятность отказа является космически низкой (например, примерно 10 ^ -24). Есть всевозможные теоретические причины, почему, но без отступления в квантовой физике и теории информации это не может быть действительно рассмотрено здесь кратко, однако до сих пор не найдено математического доказательства, чтобы окончательно доказать, что это так, все оценки и практика. Но каждый аромат unix полагался на эти оценки уже более 30 лет для надежной асинхронной связи.

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

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

Наконец, если вам нужно иметь дело с большими или пакетированными сообщениями, перемещающимися на большом расстоянии от извилистой сети или в ядре и из него, шаблон дизайна должен добавить исходный адрес для обратных вызовов и временную метку, а также немного коррекция, поэтому TCP/IP, SMQ, Netbios и т.д. включают их в свои протоколы, поэтому вам нужно будет сделать так, чтобы вы изменили очередь приоритетов/заказов, чтобы быть осведомленными о пакете.

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

Надеюсь, я ответил на ваш вопрос и не отклонился от того, что вы просили.:)

Post-Edit:

Вот несколько хороших примеров того, как и зачем использовать подобные типы конструкций очередей для распределенных параллельных приложений, они использовались в основе большинства распределенных дизайнерских решений FRP:

https://docs.oracle.com/cd/E19798-01/821-1841/bncfh/index.html

https://blog.codepath.com/2013/01/06/asynchronous-processing-in-web-applications-part-2-developers-need-to-understand-message-queues/

http://www.enterpriseintegrationpatterns.com/patterns/messaging/ComposedMessagingMSMQ.html

http://soapatterns.org/design_patterns/asynchronous_queuing

http://www.rossbencina.com/code/programming-with-lightweight-asynchronous-messages-some-basic-patterns

http://www.asp.net/aspnet/overview/developing-apps-with-windows-azure/building-real-world-cloud-apps-with-windows-azure/queue-centric-work-pattern

http://spin.atomicobject.com/2014/08/18/asynchronous-ios-reactivecocoa/

http://fsharpforfunandprofit.com/posts/concurrency-reactive/

и видео от Мартина Одерского...

https://www.typesafe.com/resources/video/martin-odersky---typesafe-reactive-platform

:)

Ответ 3

Используя Object.freeze, вы можете сделать объект неизменным:

var o = {foo: "bar"};
Object.freeze(o);
o.abc = "xyz";
console.log(o);

Допустим {foo: "bar"}. Обратите внимание, что попытка установить новое свойство в замороженной переменной будет терпеть неудачу.

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