Избегать состояния в простом инкрементере - PullRequest
4 голосов
/ 30 октября 2019

Я пытаюсь обернуть голову, избегая состояния, используя методы функционального программирования JavaScript. Я хорошо разбираюсь во многих базовых техниках fp, таких как замыкания, карри и так далее. Но я не могу разобраться с состоянием.

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

Пользователь нажимает кнопкув браузере (реализация jQuery в порядке). Значение на экране должно увеличиваться на 1 каждый раз, когда пользователь нажимает кнопку.

Как мы можем сделать это без изменения состояния? Если необходимо изменить состояние, какой подход будет наилучшим с функциональной точки зрения?

Ответы [ 2 ]

2 голосов
/ 31 октября 2019

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

const h1 = document.querySelector("h1");

const [decrement, reset, increment] = document.querySelectorAll("button");

const render = count => {
    h1.innerHTML = count; // output
    decrement.onclick = event => render(count - 1); // -+
    reset.onclick     = event => render(0);         //  |-- transition functions
    increment.onclick = event => render(count + 1); // -+
};

render(0);
//     ^
//     |
//     +-- initial state
<h1></h1>
<button>-</button>
<button>Reset</button>
<button>+</button>

Это пример машины Мура . Машина Мура - это конечный автомат . Он состоит из трех вещей.

  1. Исходное состояние машины. В нашем случае начальное состояние - 0.
  2. Функция перехода, которая дает текущее состояние и некоторый вход, создает новое состояние.
  3. Функция вывода, которая дает текущийсостояние, производит некоторый вывод.

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

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

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

1 голос
/ 30 октября 2019

Как видите, счетчик в не-fp режиме просто с состоянием . Он хранит состояние так, что может увеличивать или уменьшать его в соответствии с их API.

const createCounter = () => {
  let value = 0;
  
  return {
    get value() {
      return value;
    },
    increment() {
      value = value + 1;
    },
    decrement() {
      value = value - 1;
    },
  };
};


const counter = createCounter();
console.log('initial value', counter.value);

counter.increment();
counter.increment();
console.log('value after two increments', counter.value);


counter.decrement();
console.log('value after one decrement', counter.value);

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

const incrementCounter = counter => counter + 1;
const decrementCounter = counter => counter - 1;

const value = 0;
console.log('initial value', value);

const valueAfterTwoIncrements = incrementCounter(
  incrementCounter(value),
);

console.log('value after two increments', valueAfterTwoIncrements);


const valueAfterOneDecrement = decrementCounter(valueAfterTwoIncrements);

console.log('value after one decrement', valueAfterOneDecrement);

Преимущества этого подхода почти неисчислимы, функции чисты и их выходной детерминирован, поэтому тестирование очень простое и т. Д.


Вопросы и ответы:

  1. «Позвольте потребителю предоставить состояние»: функции (inc / dec) не работают с собственным состоянием, они принимают его в качестве аргумента и возвращают его новую версию. Попробуйте представить себе редукторный редуктор , они только встраивают логику для изменения состояния ... но в конечном итоге состояние передается в качестве аргумента.
  2. ", где значения продолжают увеличиваться/ decmented ": состояние никогда не меняется, это называется immutability , чистые функции всегда будут возвращать новую его копию, так что если вы захотите, вы должны сохранить ее где-то еще.

Работа с DOM

Отдельный уровень представления от фактического уровня бизнес-логики

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

/***** View Layer *****/
const IncBtn = ({ dispatch }) => {
  dispatch({ type: 'INC' });
};

const DecBtn = ({ dispatch }) => {
  dispatch({ type: 'DEC' });
};

const Value = ({ getState }) => {
  document.querySelector('#value').value = getState();
};


/***** Business Logic Layer *****/
const counter = (state = 0, { type }) => {
  switch(type) {
    case 'INC':
      return state + 1;
      
    case 'DEC':
      return state - 1;
    
    default:
      return state;
  }
};


/***** Orchestration Layer *****/
const createStore = (reducer) => {
  let state = reducer(undefined, { type: 'INIT' });
  
  return {
    dispatch: (action) => {
      state = reducer(state, action);
    },
    getState: () => state,
  };
}


(() => {
  const store = createStore(counter);
  // first render
  Value(store);
  
  document
    .querySelector('#inc')
    .addEventListener('click', () => {
      IncBtn(store);
      
      Value(store);
    });
  
  document
    .querySelector('#dec')
    .addEventListener('click', () => {
      DecBtn(store);
      
      Value(store);
    });
})();
<button id="inc">Increment</button>
<button id="dec">Decrement</button>
<hr />

<input id="value" readonly disabled/>
...