Evolve


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


obj.counter = obj.counter + 1

Проблема тут в том, что мы мутируем исходный объект, а это никак не допустимо в рамках функционального программирования, да и вне его, например в редьюсере redux вы не увидите изменений если мутировали стейт. Нужно возвращать каждый раз новый объект, например с помощью спрэд оператора ... в ES6 (деструктурирующее присваивание если быть точнее):


const newObject = {
  ...obj,
  counter: obj.counter + 1,
}

Но с высокой вложенностью начинает происходить страшный ад на экране, с огромным каскадом и обилием трех точек =) Так же в популярных либах есть хелперы в виде merge функции, например из lodash:


_.merge({}, obj, { counter: obj.counter + 1 })

Это работает очень просто, склеивает объекты слева на право, заменяя дублирующие ключи. Благо у нас уже есть возможность использовать нейтив инструмент из коробки: Object.assign(), делает тоже самое

Метод Object.assign() используется для копирования значений всех собственных перечисляемых свойств из одного или более исходных объектов в целевой объект. После копирования он возвращает целевой объект.

Писателям на реакте знакома подобная конструкция:


this.setState({ counter: this.state.counter + 1 })
// или 
this.setState({ show: !this.state.show })

В целом ок, но если не нужно передать какую то вложенность объекта:


this.setState(s => ({ 
  ...s, 
  deep: {
   counter: s.deep.counter + 1,
   show: !s.deep.show,
  }
}))

На мой взгляд начинает выглядеть громоздко, В либе ramda есть отличная функция evolve, Она принимает на вход 2 объекта и возвращает всегда новый; Ее сигнатура выглядит так: {k: (v v)} {k: v} {k: v} в человеческом языке так: evolve(объектМутатор, исходныйОбъект) Вот как она работает:


const inc = n => n + 1;
const newObj = evolve({ counter: inc }, obj)

что произошло? 1 аргументом мы передали объект, с теми ключами, значения которых мы хотим поменять, но вместо значений, мы передали наши функции первого класса, они же в свою очередь получат аргументом, значение из начального объекта(исходныйОбъект); Создадим еще примеров,


const add = a => b => a + b; // это мы помним
const inc = add(1) // это тоже
const dec = add(-1) 

const not = b => !b; // Функция меняющая булево значение на противоположное
// not(true) = false
// not(false) = true


const state = {
    counter: 0,
    show: false,
}

const stateTransformer = {
    counter: inc,
    show: not,
}

const newState = evolve(stateTransformer, state);
// newState { counter: 1,  show: true }

Функция evolve каррированая, а это значит что можно использовать ее так: evolve(объектМутатор)(исходныйОбъект)

Для меня в реакте это означает что я могу сделать что то в этом духе:


this.setState(evolve(stateTransformer));

Давайте больше примеров:


const T = () => true; // функция возвращает true
const F = () => false; // функция возвращает false
const transformDecrease = { counter: dec  };
const transformincrease = { counter: inc  };

const transformToggleShow = { show: not  };
const transformShow = { show: T };
const transformHide = { show: F };


const handleButtonInc = () => {
    this.state(evolve(transformincrease))
}

const handleButtonDec = () => {
    this.state(evolve(transformDecrease))
}

const showCounter = () => {
    this.setState(evolve(transformShow))
}
// можно комбинировать трансформеры, как обычные объекты
const showAdnInc = () => {
    this.setState(merge(transformShow, transformincrease));
}

// или передавать в композицию:

const x = pipe(
    evolve(transformShow),
    evolve(transformincrease),
)
this.setState(x);
// тоже самое, что :
this.setState(evolve(transformincrease, evolve(transformShow)))


evolve это лишь один из способов изменить объект, простой, удобный, и красивый, опять же, по моему мнению =)