Рассмотрели абстракции, линзы и каррированные функции в Redux, слегка коснувшись комбинаторного программирования.
801 открытий2К показов
В первой части статьи Декларативный JavaScript мы рассмотрели конструкцию switch/case и представили её в виде функций высшего порядка. В этой части мы продолжим развивать эту идею и применим её в создании редьюсеров Redux.
Несмотря на то, что популярность библиотеки Redux уже стихла, она идеально подходит для разбора примеров по двум причинам: во-первых, многие знакомы с этой библиотекой, а во-вторых, код, который создаётся вокруг этой библиотеки, можно упростить и сделать читабельнее.
Абстракции
Возможно, вы замечали, что если указать собаке пальцем на предмет, она будет смотреть на кончик пальца, а не на сам предмет. Для собаки сложно представить воображаемую линию между кончиком пальца и предметом. В её сознании такая абстракция не существует. К счастью, у нас есть такой навык и он нам позволяет упрощать сложные вещи и представлять их в простой форме.
Например, на диаграмме выше нарисован квадратик, который символизирует приложение, а бочка, присоединенная к квадратику, означает базу данных. Такие диаграммы помогают нам понять сложные архитектурные решения, не углубляясь в детали реализации. Эту же методику можно применить и в программировании, выделяя основной код от второстепенного, пряча их за функциями.
Создание редьюсера подобно оглавлению книги: оно позволяет быстро найти необходимую функцию по типу экшена. Функции, такие как signIn и signOut, иллюстрируют преобразования состояния объекта: они принимают на вход полезную нагрузку и текущее состояние объекта. Второстепенный код, включающий проверку типов экшена, вызов нужной функции и прочее, скрыты за функциями createReducer и on.
В функциональном программировании, линзы – это абстракции, которые позволяют работать с вложенными структурами данных, такими как объекты или массивы. Проще говоря это иммутабельные сеттеры и геттеры. Эти функции называются линзами, потому что они позволяют сфокусироваться на значении определенной ветки объекта:
Для начало давайте взглянем как обновляется значение объекта без линз:
Как уже мы выяснили, каррированные функции очень удобны при композиции функций или передаче переменных в контекст функции из разных мест. Но они выглядят неуклюже, когда требуется передать все параметры сразу. Например, каррированая версия set будет выглядеть так:
set('user.address.city')(name)(state)
Можно написать функцию curry, которая позволит преобразовать любую функцию в каррированную и позволит вызывать её с разным количеством параметров:
Вместо этого просто нужно передать функцию в качестве параметра. Однако стоит помнить, что функция then ожидает колбэк-функцию с двумя параметрами. Поэтому прямая передача будет безопасной только если setUsers ожидает только один параметр.
Но самое главное, теперь функцию set можно включить в композицию и выполнять несколько обновлений:
const signIn = pincode => state => ({
attempts: state.attempts + 1
isSigned: pincode === '2023',
})
// 'state => ({ ...' is removed
const signIn = pincode => compose(
set('attempts', attempts => attempts + 1),
set('isSigned', pincode === '2023'),
)
const updateCity = name => state => ({
lastUpdate: new Date(),
user: {
...state.user,
address: {
...state.user.address,
city: name,
}
}
})
// ⮁ it equals
const updateCity = name => compose(
set('lastUpdate', new Date()),
set('user.address.city', name),
)
Такой стиль программирования называется комбинаторным (point free) и широко используется в функциональном программировании.
Эта статья получилась немного длиннее, чем планировалось, но я надеюсь, что вы узнали что-то новое для себя. В следующей статье мы представим конструкцию try/catch в виде функции. И, как обычно, небольшой тизер к следующему посту:
const result = trap(unstableCode)
.pass(x => x * 2)
.catch(() => 3)
.release(x => x * 10)