1. Первые шаги
2. Сочетаем функции
3. Частичное применение (каррирование)
4. Декларативное программирование
5. Бесточечная нотация
6. Неизменяемость и объекты
7. Неизменяемость и массивы
8. Линзы
9. Заключение
Данный пост — это седьмая часть серии статей о функциональном программировании под названием "Мышление в стиле Ramda".
В шестой части мы говорили о работе с объектами JavaScript в функциональном и иммутабельном стиле.
В данном посте мы поговорим о подобной работе с массивами.
Чтение элементов массива
В шестой части мы узнали о различных функциях Ramda, предназначенных для чтения свойств объектов, таких как prop
, pick
и has
. Ramda имеет ещё больше методов для чтения элементов массивов.
Эквивалент prop
для массива — это nth; эквивалент для pick
— это slice, и эквивалент для has
— это contains. Давайте взглянем на них.
const numbers = [10, 20, 30, 40, 50, 60]
nth(3, numbers) // => 40 (индексы с нуля)
nth(-2, numbers) // => 50 (отрицательные числа стартуют с конца массива)
slice(2, 5, numbers) // => [30, 40, 50] (см. ниже)
contains(20, numbers) // => true
Slice берёт два индекса и возвращает подмассив, который начинается на первом индексе (начиная с нуля) и включает все элементы до второго индекса, но не включая элемент этого индекса.
Получение доступа к первому и последнему элементам массива довольно распространено, так что Ramda предоставляет короткие функции для этих случаев, head и last. Она также предоставляет функции для получения всех элементов, кроме первого (tail), всех, кроме последнего (init), первых N элементов (take(N)), и последних N элементов (takeLast(N)). Давайте взглянем на них в действии.
const numbers = [10, 20, 30, 40, 50, 60]
head(numbers) // => 10
tail(numbers) // => [20, 30, 40, 50, 60]
last(numbers) // => 60
init(numbers) // => [10, 20, 30, 40, 50]
take(3, numbers) // => [10, 20, 30]
takeLast(3, numbers) // => [40, 50, 60]
Добавляем, обновляем и удаляем элементы массива
Изучая работу с объектами, мы узнали о функциях assoc
, dissoc
и omit
для добавления, обновления и удаления свойств.
Так как массивы имеют упорядоченную структуру данных, у нас есть несколько методов, которые выполняют ту же работу, что и assoc
для объектов. Наиболее распространённые — это insert и update, но Ramda также предоставляет методы append и prepend для типичных случаев добавления элементов в начало и конец массива. insert
, append
, и prepend
добавляют новые элементы в массив; update
"заменяет" определённый элемент в массиве новым значением.
Как вы можете ожидать от функциональной библиотеки, все эти функции возвращают новый массив с ожидаемыми изменениями; оригинальный же массив никогда не изменяется.
const numbers = [10, 20, 30, 40, 50, 60]
insert(3, 35, numbers) // => [10, 20, 30, 35, 40, 50, 60]
append(70, numbers) // => [10, 20, 30, 40, 50, 60, 70]
prepend(0, numbers) // => [0, 10, 20, 30, 40, 50, 60]
update(1, 15, numbers) // => [10, 15, 30, 40, 50, 60]
Для объединения двух объектов в один, мы ранее узнали о методе merge
. Ramda также предоставляет метод concat для выполнения той же операции с массивами.
const numbers = [10, 20, 30, 40, 50, 60]
concat(numbers, [70, 80, 90]) // => [10, 20, 30, 40, 50, 60, 70, 80, 90]
Обратите внимание, что второй массив присоединился к первому. Это выглядит логичным при использовании этого метода в отдельности от другого кода, но, также как и с merge
, эта логика может привести не совсем к тому, что мы хотели бы ожидать, если мы будем использовать этот метод в нашем конвеере. Я нашёл полезным для себя написание функции-помощника, concatAfter
: const concatAfter = flip(concat)
, чтобы использовать её в своих конвеерах.
Ramda также предоставляет несколько возможностей для удаления элементов. remove удаляет элементы по их индексу, в то время как without удаляет их по их значению. Также есть такие методы как drop и dropLast для типичных случаев, когда мы удаляем элементы из начала или конца массива.
const numbers = [10, 20, 30, 40, 50, 60]
remove(2, 3, numbers) // => [10, 20, 60]
without([30, 40, 50], numbers) // => [10, 20, 60]
drop(3, numbers) // => [40, 50, 60]
dropLast(3, numbers) // => [10, 20, 30]
Обратите внимание, что remove
принимает индекс и количество, в то время как slice
принимает два индекса. Эта неконсистентность может сбить с толку, если вы не будете знать об этом.
Преобразование элементов
Также как и с объектами, мы можем пожелать обновить элемент массива, применив функцию к оригинальному значению.
const numbers = [10, 20, 30, 40, 50, 60]
// умножим третий элемент массива на 10
update(2, multiply(10, nth(2, numbers)), numbers) // => [10, 20, 300, 40, 50, 60]
Чтобу упростить этот типичный случай, Ramda предоставляет метод adjust, который работает очень похожим образом на evolve
для объектов. Но в отличии от evolve
, adjust
работает только с одним элементом массива.
const numbers = [10, 20, 30, 40, 50, 60]
// умножим третий элемент массива на 10
adjust(multiply(10), 2, numbers)
Обратите внимание, что первые два аргумента к adjust
идут наоборот, если сравнивать их с update
. Это может быть источником ошибок, но оно имеет смысл тогда, когда вы будете рассматривать частичное применение. Вы можете пожелать сделать для себя функцию изменения adjust(multiply(10))
и впоследствии решить, какой именно индекс массива изменять с помощью неё.
Заключение
Теперь у нас есть инструменты для работы с массивами и объектами в декларативном и иммутабельном стиле. Это позволяет нам строить программы, состоящие из маленьких, функциональных строительных блоков, совмещая функции, которые будут делать то, что нам нужно, и всё это без изменения всех наших структур данных.
Далее
Мы изучили способы чтения, обновления и трансформации свойств объектов и элементов массивов. Ramda предоставляет и другие основные инструменты для выполнения данных операций, линзы. Следующая статья о линзах должна будет показать нам, как они работают.