Хабрахабр

[Перевод] Мышление в стиле Ramda: Неизменяемость и объекты

Первые шаги
2. 1. Частичное применение (каррирование)
4. Сочетаем функции
3. Бесточечная нотация
6. Декларативное программирование
5. Неизменяемость и массивы
8. Неизменяемость и объекты

7. Заключение Линзы
9.

Данный пост — это шестая часть серии статей о функциональном программировании под названием "Мышление в стиле Ramda".

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

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

Чтение свойств объекта

Давайте снова взглянем на пример с определением людей, имеющих право голоса, который мы рассматривали в пятой части:

const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY
const wasNaturalized = person => Boolean(person.naturalizationDate)
const isOver18 = person => person.age >= 18 const isCitizen = either(wasBornInCountry, wasNaturalized)
const isEligibleToVote = both(isOver18, isCitizen)

Как вы можете видеть, мы сделали isCitizen и isEligibleToVote бесточечными, но мы не можем сделать это с первыми тремя функциями.

Начнём с этого: Как мы узнали в четвёртой части, мы можем сделать наши функции более декларативными через использование equals и gte.

const wasBornInCountry = person => equals(person.birthCountry, OUR_COUNTRY) const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => gte(person.age, 18)

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

prop

Она предоставляет функцию prop для получения доступа к свойствам объектов. К счастью, Ramda в очередной раз приходит к нам на помощь.

Давайте сделаем это: Используя prop, мы можем переписать person.birthCountry в prop('birthCountry', person).

const wasBornInCountry = person => equals(prop('birthCountry', person), OUR_COUNTRY) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(prop('age', person), 18)

Но давайте продолжим наш рефакторинг. Воу, сейчас оно выглядит как-то намного хуже. equals работает точно также в обратном порядке, так что мы ничего не сломаем: Давайте изменим порядок аргументов, которые мы передаём в equals, чтобы prop шёл последним.

const wasBornInCountry = person => equals(OUR_COUNTRY, prop('birthCountry', person)) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(prop('age', person), 18)

Далее, давайте используем каррирование, природное свойство equals и gte, для того чтобы создать новые функции, к которым будет применяться результат вызова prop:

const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry', person)) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(__, 18)(prop('age', person))

Давайте применим преимущество каррирования снова для всех вызовов prop: Это всё ещё выглядит более худшим вариантом, но всё же давайте продолжим.

const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry')(person)) const wasNaturalized = person => Boolean(prop('naturalizationDate')(person)) const isOver18 = person => gte(__, 18)(prop('age')(person))

Но теперь мы видим знакомый паттерн. Снова как-то не очень. Все наши функции имеют тот самый образ f(g(person)), и как мы знаем из второй части, это эквивалентно compose(f, g)(person).

Давайте применим это преимущество к нашему коду:

const wasBornInCountry = person => compose(equals(OUR_COUNTRY), prop('birthCountry'))(person) const wasNaturalized = person => compose(Boolean, prop('naturalizationDate'))(person) const isOver18 = person => compose(gte(__, 18), prop('age'))(person)

Все наши функции выглядят как person => f(person). Теперь мы кое-что получили. И мы уже знаем из пятой части, что мы можем сделать эти функции бесточечными.

const wasBornInCountry = compose(equals(OUR_COUNTRY), prop('birthCountry')) const wasNaturalized = compose(Boolean, prop('naturalizationDate')) const isOver18 = compose(gte(__, 18), prop('age'))

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

Давайте взглянем на некоторые другие инструменты, которые Ramda предоставляет для работы с объектами.

pick

Там, где prop читает одно свойство объекта и возвращает его значение, pick читает множество свойств из объекта и возвращает новый объект только с ними.

К примеру, если нам нужны только имена и годы персон, мы можем использовать pick(['name','age'], person).

has

Если мы просто хотим знать, что наш объект имеет свойство, без чтения его значения, мы можем использовать функцию has для проверки его свойств, а также hasIn для проверки цепочки прототипов: has('name', person).

path

К примеру, мы хотим вытащить почтовый индекс из более глубокой структуры: path(['address','zipCode'], person). Там, где prop читал свойство объекта, path углубляется во вложенные объекты.

path вернёт undefined, если что-либо на пути (включая оригинальный аргумент) окажется в значении null или undefined, в то время как prop в таких ситуациях вызовет ошибку. Обратите внимание на то, что path более прощающий, чем prop.

propOr / pathOr

Они предоставляют вам возможность указать значение по умолчанию для свойства или пути, которые не будут найдены в изучаемом объекте. propOr и pathOr подобны prop и path, совмещённым с defaultTo.

Обратите внимание, что в отличии от prop, propOr не будет вызывать ошибку, если person окажется равен null или undefined; вместо этого он вернёт значение по умолчанию. К примеру, мы можем предоставить заполнитель, когда мы не знаем имени персоны: propOr('<Unnamed>, 'name', person).

keys / values

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

Добавление, обновление и удаление свойств

Теперь у нас есть множество инструментов для чтения из объектов в декларативном стиле, но что насчёт внесения изменений?

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

И снова Ramda предоставляет нам множество полезностей.

assoc / assocPath

Когда мы программируем в императивном стиле, мы можем установить или изменить имя персоны через оператор присваивания: person.name = 'New name'.

В нашем функциональном, неизменяемом мире, вместо этого мы можем использовать assoc: const updatedPerson = assoc('name', 'newName', person).

assoc возвращает новый объект с добавленным или обновлённым значением свойства, оставляя оригинальный объект неизменённым.

У нас также имеется в распоряжении assocPath для обновления вложенного свойства: const updatedPerson = assocPath(['address', 'zipCode'], '97504', person).

dissoc / dissocPath / omit

Императивно, мы можем захотеть сказать delete person.age. Что насчёт удаления свойств? В Ramda, мы будем использовать dissoc: `const updatedPerson = dissoc('age', person)

dissocPath примерно о том же, но работает на более глубоких структурах объектов: dissocPath(['address', 'zipCode'], person).

А также у нас имеется omit, который может удалить несколько свойств за раз: const updatedPerson = omit(['age', 'birthCountry'], person).

Они очень удобны для белых списков (сохранять только определённый набор свойств, используя pick) и чёрных списков (избавляться от определённых свойств через использование omit). Обратите внимание, что pick и omit немного похожи и очень красиво дополняют друг друга.

Трансформация объектов

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

const nextAge = compose(inc, prop('age')) const celebrateBirthday = person => assoc('age', nextAge(person), person)

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

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

Давайте отрефакторим celebrateBirthday на использование evolve: Ramda ещё раз спасает нас с функцией evolve.evolve принимает объект и позволяет указать функции трансформации для тех свойств, которые мы желаем изменить.

const celebrateBirthday = evolve()

Данный код говорит, что мы преобразуем указанный объект (который не отображается в силу бесточечного стиля) через создание нового объекта с теми же свойствами и значениями, но свойство age будет получено через применение inc к оригинальному значению свойства age.

Преобразование объекта может иметь тот же образ, какой будет иметь изменяемый объект, и evolve будет рекурсивно проходить между структурами, применяя функции трансформации в указанном виде. evolve может преобразовать множество свойств за раз и даже на множественных уровнях вложенности.

Обратите внимание, что evolve не добавляет новые свойства; если вы укажете трансформацию для свойства, которое не встречается в обрабатываемом объекте, evolve просто проигнорирует его.

Я обнаружил, что evolve быстро становится рабочей лошадкой в моих приложениях.

Слияние объектов

Типичный случай — когда у вас есть функция, которая берёт именованные опции, и вам хочется объединить их с опциями по умолчанию. Иногда вам нужно объединить два объекта вместе. Ramda предоставляет функцию merge для этой цели.

function f(a, b, options = {}) { const defaultOptions = { value: 42, local: true } const finalOptions = merge(defaultOptions, options)
}

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

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

Попытка просто использовать merge(newValues) в конвеере не даст того, что мы хотели бы получить.

Она может быть написана как const reverseMerge = flip(merge). Для этой ситуации, я обычно создаю свою утилиту под названием reverseMerge. Вызов flip меняет местами первые два аргумента функции, которая к нему применяется.

Если объекты при объединении имеют свойство, значение которого является подобъектом — то эти подобъекты не сливаются. merge выполняет поверхностное слияние. На сегодняшний день в Ramda имеются такие функции как mergeDeepLeft, mergeDeepRight для рекурсивного глубокого слияния объектов, а также другие методы для слияний). Ramda в данный момент не имеет способности "глубокого слияния" (оригинальная статья, перевод которой я делаю, уже имеет устаревшую информацию по данной теме.

Если у вас есть желание объединить множество объектов в один, вы можете использовать mergeAll, который принимает массив объектов для объединения. Обратите внимание, что merge принимает только два аргумента.

Заключение

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

Далее

"Иммутабельность и массивы" расскажет нам, что делать с ними. Теперь мы можем работать с объектами в иммутабельном стиле, но что насчёт массивов?

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть