- PVSM.RU - https://www.pvsm.ru -
1. Первые шаги [1]
2. Сочетаем функции [2]
3. Частичное применение (каррирование) [3]
4. Декларативное программирование [4]
5. Бесточечная нотация
6. Неизменяемость и объекты
7. Неизменяемость и массивы
8. Линзы
9. Заключение
Данный пост является четвёртой частью серии о функциональном програмировании под названием «Мышление в стиле Ramda».
В третьей части мы говорили об объединении функций, которые могут принимать больше одного аргумента, используя техники частичного применения и каррирования.
Когда мы начинаем писать маленькие функциональные строительные блоки и объединять их, мы обнаруживаем, что нам необходимо написать множество функций, которые будут оборачивать операторы JavaScript, такие как арифметика, сравнение, логика и управление потоком. Это может показаться утомительным, но мы находимся за спиной Ramda.
Но сначала, небольшое введение.
Есть множество различных путей для разделения языков программирования и стилей написания. Это статическая типизация против динамической типизаци, интерпретируемые языки и компилируемые языки, высокоуровневые и низкоуровные, и так далее.
Другое подобное разделение заключается в императивном програмировании против декларативного.
Без погружения вглубь этого, императивное программирование — это стиль программирования, в котором программисты говорят компьютеру, что нужно сделать, объясняя ему, как это нужно сделать. Императивное программирование даёт множество конструкций, которые мы используем каждый день: управление потоком (if
-then
-else
синтаксис и циклы), арифметические операторы (+
, -
, *
, /
), операторы сравнения (===
, >
, <
, и т.д.), и логические операторы (&&
, ||
, !
).
Декларативное программирование — это стиль програмирования, в котором програмисты говорят компьютеру, что нужно сделать, объясняя ему, что они хотят. Компьютер далее должен определить, как получить необходимый результат.
Один из классических декларативных языков — это Prolog. В Prolog програма состоит из набора фактов и набора правил вывода. Вы начинаете программу, задавая вопрос, и набор правил вывода Prolog'а использует факты и правила для ответа на ваш вопрос.
Функциональное программирование рассматривается как подмножество декларативного програмирования. В функциональной программе, мы объявляем функции и далее объясняем компьютеру что мы хотим сделать, совмещая данные функции.
Даже в декларативных программах необходимо выполнять подобные задачи, которые мы выполняем в императивных программах. Управление потоком, арифметика, сравнения и логика всё ещё являются базовыми строительными блоками, с которыми мы должны работать. Но нам необходимо найти способы для выражения этих конструкций в декларативном стиле.
Поскольку мы программируем на JavaScript, императивном языке, это нормально — использовать стандартные императивные конструкции при написании «нормального» JavaScript кода.
Но когда мы пишем функциональные трансформации, используя конвееры и подобные им конструкции, императивные конструкции перестают вписываться с создаваемую структуру кода.
Посмотрим на некоторые базовые строительные блоки, которые предоставляет Ramda для того чтобы помочь нам выйти из этой неприятной ситуации.
Во второй части [2] мы реализовали серию арифметических трансформаций для демонстрации конвеера:
const multiply = (a, b) => a * b
const addOne = x => x + 1
const square = x => x * x
const operate = pipe(
multiply,
addOne,
square
)
operate(3, 4) // => ((3 * 4) + 1)^2 => (12 + 1)^2 => 13^2 => 169
Обратите внимание, как мы пишем функции для всех базовых строительных блоков, которые мы желаем использовать.
Рамда предоставляет функции add [5], subtract [6], multiply [7] и divide [8] для использования в местах стандартных арифметических операций. Так что мы можем использовать рамдовскую multiply
там, где мы использовали самописную функцию, мы можем взять преимущество каррированной функции add
для замены нашей addOne
, и мы также можем написать squade
с помощью multiply
.
const square = x => multiply(x, x)
const operate = pipe(
multiply,
add(1),
square
)
add(1)
очень похожа на оператор инкрементирования (++
), но оператор инкрементирования изменяет переменную, так что он вызывает мутацию. Как мы узнали из первой части [1], иммутабельность — это основной принцип функционального программирования, так что мы не хотим использовать ++
или его кузена --
.
Мы можем использовать add(1)
и subtract(1)
для увеличения и уменьшения, но так как эти две операции такие распространённые, Ramda предоставляет inc [9] и dec [10] вместо них.
Так что мы можем ещё немного упростить наш конвеер:
const square = x => multiply(x, x)
const operate = pipe(
multiply,
inc,
square
)
subtract
является заменой бинарного оператора -
, но у нас ещё имеется унарный оператор -
для отрицания значения. Мы также можем использовать multiply(-1)
, но Ramda предоставляет функцию negate [11] для выполнения этой задачи.
Также во второй части [2] мы написали несколько функций для определения, является ли персона имеющей право на голосование. Конечная версия того кода выглядела следующим образом:
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)
Обратите внимание, что некоторые из наших функций использут стандартные операторы сравнения (===
и >=
в данном случае). Как вы можете предположить сейчас, Ramda также предоставляет заменители для всего этого.
Давайте преобразуем наш код на использование equals [12] вместо ===
и gte [13] вместо >=
.
const wasBornInCountry = person => equals(person.birthCountry, OUR_COUNTRY)
const wasNaturalized = person => Boolean(person.naturalizationDate)
const isOver18 = person => gte(person.age, 18)
const isCitizen = either(wasBornInCountry, wasNaturalized)
const isEligibleToVote = both(isOver18, isCitizen)
Ramda также предоставляет gt [14] для >
, lt [15] для <
и lte [16] для <=
Обратите внимание, что эти функции, как кажется, принимают свои аргументы в нормальном порядке (первый аргумент больше второго?). Это имеет смысл, когда мы используем их в изоляции, но может сбивать с толку при объединении функций. Эти функции нарушают принцип «данные идут последними», так что нам нужно быть осторожными, когда мы используем иих в наших конвеерах и подобных им ситуациях. И это место, когда flip [17] и заполнитель (__ [18]) становятся полезными.
В дополнение к equals
есть ещё identical [19] для определения, являются ли два значения ссылками на то же пространство в памяти.
Существует набор случаев основных применений для ===
: проверка, что строка или массив являются пустыми (str === ''
или arr.length === 0
) и проверка, является ли переменная равной null
или undefined
. Ramda предоставляет удобные функции для обоих случаев: isEmpty [20] и isNil [21].
Во второй части [2] (и чуть выше), мы использовали функции both
и either
в местах операторов &&
и ||
. Мы также говорили о complement
для мест с !
.
Эти комбинированные функции работают прекрасно, когда функции объединяют операцию над тем же значением. Написанные выше wasBornInCountry
, wasNaturalized
и isOver18
все применялись к объекту персоны.
Но иногда нам нужно применить &&
, ||
и !
к различным значениям. Для подбоных случаев Ramda предоставляет нам функции and [22], or [23] и not [24]. Я думаю следующим образом: and
, or
и not
работают со значениями, в то время как both
, either
и complement
работают с функциями.
В основном, ||
используется для получения значений по умолчанию. К примеру, мы можем написать что-нибудь вроде этого:
const lineWidth = settings.lineWidth || 80
Это распространённая идиома, и чаще всего работающая, но полагающаяся на JavaScript логику определения «ложности». Что если 0
является валидным параметром? Так как 0
является ложным значением, мы получим значение линии равное 80.
Мы можем использовать функцию isNil
, о которой мы только что узнали выше, но Ramda снова имеет более логичный вариант для нас: defaultTo [25].
const lineWidth = defaultTo(80, settings.lineWidth)
defaultTo
проверяет второй аргумент на isNil
. Если проверка провалилась он вернёт полученное значение, в ином случае вернёт первый аргумент, переданный ей.
Управление потоком выполнения менее важно в функциональном программировании, но иногда оказывается нужным. Коллекция итерирующих функций, о которых мы говорили в первой части [1], заботиться о большинстве ситуаций с циклами, но условия всё ещё довольно важны.
Давайте напишем функцию, forever21
, которая получает год и возвращает следующий. Но, как нам указывает её имя, начиная с 21 года, он будет оставаться в этом значении.
const forever21 = age => age >= 21 ? 21 : age + 1
Обратите внимание, что наше условие (age >= 21
) и вторая ветвь (age + 1
) могут быть обе написаны как функции age
. Мы можем переписать первую ветвь (21
) как функцию-константу (() => 21
). Теперь у нас будет три функции, которые принимают (или игнорируют) age
.
Теперь мы на позиции, когда мы можем использовать функцию isElse
из Ramda, которая является эквивалентом структуры if...then..else
или её более короткого кузена, тернарного оператора (?:
).
const forever21 = age => ifElse(gte(__, 21), () => 21, inc)(age)
Как мы упомянули выше, функции сравнения не работают подобно функциям объединения, так что здесь нам нужно начать использовать заполнитель (__
). Мы также можем применить lte
вместо этого:
const forever21 = age => ifElse(lte(21), () => 21, inc)(age)
В данном случае, мы должны читать это как «21 меньше или равно age
». Я собираюсь придерживаться версии с заменителем в оставшейся части поста, так как я нахожу это более читабельным и менее запутывающим.
Функции-константы весьма полезны в ситуациях, подобных этой. Как вы можете предположить, Ramda предоставляет нам сокращение. В данном случае, сокращение называется always [26].
const forever21 = age => ifElse(gte(__, 21), always(21), inc)(age)
Ramda также предоставляет T [27] и F [28] в качестве дальнейших сокращений для always(true)
и always(false)
Давайте попробуем написать другую функцию, alwaysDrivingAge
. Эта функция принимает age
и возвращает его, если его значение gte
16. Если же оно меньше 16, то она вернёт 16. Это позволяет любому притвориться, что он управляет возрастом, даже если это не так:
const alwaysDrivingAge = age => ifElse(lt(__, 16), always(16), a => a)(age)
Вторая ветвь сравнения (a => a
) — это другой типичный паттерн в функциональном программировании. Это известно как «тождественность» (не знаю точного перевода термина «identity function», просто выберу этот — прим. пер.). То есть, это функция, которая просто возвращает тот аргумент, который она получила.
Как вы уже можете ожидать, Ramda предоставляет нам функцию identity [29]:
const alwaysDrivingAge = age => ifElse(lt(__, 16), always(16), identity)(age)
identity
может принять больше одного аргумента, но всегда вернёт только первый. Если мы хотим вернуть что-то другое, отличное от первого аргумента, для этого существует более общая функция nthArg [30]. Это гораздо менее распространённая ситуация, чем использование identity
.
Выражение isElse
, в котором одна из логических ветвей является тождественностью, также является типичным патеррном, так что Ramda предоставляет нам больше сокращающих методов.
Если, как в нашем случае, вторая ветвь является тождественностью, мы можем использовать when [31] вместо ifElse
:
const alwaysDrivingAge = age => when(lt(__, 16), always(16))(age)
Если первая ветвь условия является тождественностью, мы можем использовать unless [32]. Если мы перевернём наше условие на использование gte(__, 16)
, мы можем использовать unless
.
const alwaysDrivingAge = age => unless(gte(__, 16), always(16))(age)
Ramda также предоставляет функцию cond [33], которая может заменить выражение switch
или цепочку выражений if...then...else
.
const water = temperature => cond([
[equals(0), always('water freezes at 0°C')],
[equals(100), always('water boils at 100°C')],
[T, temp => `nothing special happens at ${temp}°C`]
])(temperature)
Мне не понадобилось использовать cond
в моём коде с Ramda, но я писал подобный код на Lisp много лет назад, так что cond
чувствуется старым другом.
Мы рассмотрели набор функций, которые Ramda предоставляет нам для превращения нашего императивного кода в декларативный функциональный код.
Вы могли заметить, что последние несколько функций, которые мы написали (forever21
, drivingAge
и water
) все принимают параметры, создают новую функцию и далее применяют эту функцию к параметру.
Это распространённый паттерн, и вновь Ramda предоставляем нам инструменты для того чтобы привести всё это к более чистому виду. Следующий пост, «Бесточечная нотация» рассматривает способы, позволяющие упростить функции, следующие подобному паттерну.
Автор: Роман Ахмадуллин
Источник [34]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/277810
Ссылки в тексте:
[1] 1. Первые шаги: https://habrahabr.ru/post/348868/
[2] 2. Сочетаем функции: https://habrahabr.ru/post/348976/
[3] 3. Частичное применение (каррирование): https://habrahabr.ru/post/349140/
[4] 4. Декларативное программирование: https://habrahabr.ru/post/349674/
[5] add: http://ramdajs.com/docs/#add
[6] subtract: http://ramdajs.com/docs/#subtract
[7] multiply: http://ramdajs.com/docs/#multiply
[8] divide: http://ramdajs.com/docs/#divide
[9] inc: http://ramdajs.com/docs/#inc
[10] dec: http://ramdajs.com/docs/#dec
[11] negate: http://ramdajs.com/docs/#negate
[12] equals: http://ramdajs.com/docs/#equals
[13] gte: http://ramdajs.com/docs/#gte
[14] gt: http://ramdajs.com/docs/#gt
[15] lt: http://ramdajs.com/docs/#lt
[16] lte: http://ramdajs.com/docs/#lte
[17] flip: http://ramdajs.com/docs/#flip
[18] __: http://ramdajs.com/docs/#__
[19] identical: http://ramdajs.com/docs/#identical
[20] isEmpty: http://ramdajs.com/docs/#isEmpty
[21] isNil: http://ramdajs.com/docs/#isNil
[22] and: http://ramdajs.com/docs/#and
[23] or: http://ramdajs.com/docs/#or
[24] not: http://ramdajs.com/docs/#not
[25] defaultTo: http://ramdajs.com/docs/#defaultTo
[26] always: http://ramdajs.com/docs/#always
[27] T: http://ramdajs.com/docs/#T
[28] F: http://ramdajs.com/docs/#F
[29] identity: http://ramdajs.com/docs/#identity
[30] nthArg: http://ramdajs.com/docs/#nthArg
[31] when: http://ramdajs.com/docs/#when
[32] unless: http://ramdajs.com/docs/#unless
[33] cond: http://ramdajs.com/docs/#cond
[34] Источник: https://habrahabr.ru/post/349674/?utm_source=habrahabr&utm_medium=rss&utm_campaign=349674
Нажмите здесь для печати.