1. Первые шаги
2. Сочетаем функции
3. Частичное применение (каррирование)
4. Декларативное программирование
5. Бесточечная нотация
6. Неизменяемость и объекты
7. Неизменяемость и массивы
8. Линзы
9. Заключение
Данный пост является четвёртой частью серии о функциональном програмировании под названием «Мышление в стиле Ramda».
В третьей части мы говорили об объединении функций, которые могут принимать больше одного аргумента, используя техники частичного применения и каррирования.
Когда мы начинаем писать маленькие функциональные строительные блоки и объединять их, мы обнаруживаем, что нам необходимо написать множество функций, которые будут оборачивать операторы JavaScript, такие как арифметика, сравнение, логика и управление потоком. Это может показаться утомительным, но мы находимся за спиной Ramda.
Но сначала, небольшое введение.
Есть множество различных путей для разделения языков программирования и стилей написания. Это статическая типизация против динамической типизаци, интерпретируемые языки и компилируемые языки, высокоуровневые и низкоуровные, и так далее.
Другое подобное разделение заключается в императивном програмировании против декларативного.
Без погружения вглубь этого, императивное программирование — это стиль программирования, в котором программисты говорят компьютеру, что нужно сделать, объясняя ему, как это нужно сделать. Императивное программирование даёт множество конструкций, которые мы используем каждый день: управление потоком (
Декларативное программирование — это стиль програмирования, в котором програмисты говорят компьютеру, что нужно сделать, объясняя ему, что они хотят. Компьютер далее должен определить, как получить необходимый результат.
Один из классических декларативных языков — это Prolog. В Prolog програма состоит из набора фактов и набора правил вывода. Вы начинаете программу, задавая вопрос, и набор правил вывода Prolog'а использует факты и правила для ответа на ваш вопрос.
Функциональное программирование рассматривается как подмножество декларативного програмирования. В функциональной программе, мы объявляем функции и далее объясняем компьютеру что мы хотим сделать, совмещая данные функции.
Даже в декларативных программах необходимо выполнять подобные задачи, которые мы выполняем в императивных программах. Управление потоком, арифметика, сравнения и логика всё ещё являются базовыми строительными блоками, с которыми мы должны работать. Но нам необходимо найти способы для выражения этих конструкций в декларативном стиле.
Поскольку мы программируем на JavaScript, императивном языке, это нормально — использовать стандартные императивные конструкции при написании «нормального» JavaScript кода.
Но когда мы пишем функциональные трансформации, используя конвееры и подобные им конструкции, императивные конструкции перестают вписываться с создаваемую структуру кода.
Посмотрим на некоторые базовые строительные блоки, которые предоставляет Ramda для того чтобы помочь нам выйти из этой неприятной ситуации.
Во второй части мы реализовали серию арифметических трансформаций для демонстрации конвеера:
Обратите внимание, как мы пишем функции для всех базовых строительных блоков, которые мы желаем использовать.
Рамда предоставляет функции add, subtract, multiply и divide для использования вместо стандартных арифметических операций. Так что мы можем использовать рамдовскую
Мы можем использовать
Так что мы можем ещё немного упростить наш конвеер:
Также во второй части мы написали несколько функций для определения, является ли персона имеющей право на голосование. Конечная версия того кода выглядела следующим образом:
Обратите внимание, что некоторые из наших функций использут стандартные операторы сравнения (
Давайте преобразуем наш код на использование equals вместо
Ramda также предоставляет gt для
Обратите внимание, что эти функции, как кажется, принимают свои аргументы в нормальном порядке (первый аргумент больше второго?). Это имеет смысл, когда мы используем их в изоляции, но может сбивать с толку при объединении функций. Эти функции нарушают принцип «данные идут последними», так что нам нужно быть осторожными, когда мы используем их в наших конвеерах и подобных им ситуациях. И именно здесь flip и заполнитель (__) могут принести пользу.
В дополнение к
Существует набор случаев основных применений для
Во второй части (и чуть выше), мы использовали функции
Эти комбинированные функции работают прекрасно, когда функции объединяют операцию над тем же значением. Написанные выше
Но иногда нам нужно применить
В основном,
Это распространённая идиома, и чаще всего работающая, но полагающаяся на JavaScript логику определения «ложности». Что если
Мы можем использовать функцию
Управление потоком выполнения менее важно в функциональном программировании, но иногда оказывается нужным. Коллекция итерирующих функций, о которых мы говорили в первой части, заботится о большинстве ситуаций с циклами, но условия всё ещё довольно важны.
Давайте напишем функцию,
Обратите внимание, что наше условие (
Теперь мы на позиции, когда мы можем использовать функцию ifElse из Ramda, которая является эквивалентом структуры
Как мы упомянули выше, функции сравнения не работают подобно функциям объединения, так что здесь нам нужно начать использовать заполнитель (
В данном случае, мы должны читать это как «21 меньше или равно
Функции-константы весьма полезны в ситуациях, подобных этой. Как вы можете предположить, Ramda предоставляет нам сокращение. В данном случае, сокращение называется always.
Ramda также предоставляет T и F в качестве дальнейших сокращений для
Давайте попробуем написать другую функцию,
Вторая ветвь сравнения (
Как вы уже можете ожидать, Ramda предоставляет нам функцию identity:
Выражение
Если, как в нашем случае, вторая ветвь является тождественностью, мы можем использовать when вместо
Если первая ветвь условия является тождественностью, мы можем использовать unless. Если мы перевернём наше условие на использование
Ramda также предоставляет функцию cond, которая может заменить выражение
Мне не понадобилось использовать
Мы рассмотрели набор функций, которые Ramda предоставляет нам для превращения нашего императивного кода в декларативный функциональный код.
Вы могли заметить, что последние несколько функций, которые мы написали (
Это распространённый паттерн, и вновь Ramda предоставляем нам инструменты для того чтобы привести всё это к более чистому виду. Следующий пост, "Бесточечная нотация" рассматривает способы, позволяющие упростить функции, следующие подобному паттерну.
2. Сочетаем функции
3. Частичное применение (каррирование)
4. Декларативное программирование
5. Бесточечная нотация
6. Неизменяемость и объекты
7. Неизменяемость и массивы
8. Линзы
9. Заключение
Данный пост является четвёртой частью серии о функциональном програмировании под названием «Мышление в стиле Ramda».
В третьей части мы говорили об объединении функций, которые могут принимать больше одного аргумента, используя техники частичного применения и каррирования.
Когда мы начинаем писать маленькие функциональные строительные блоки и объединять их, мы обнаруживаем, что нам необходимо написать множество функций, которые будут оборачивать операторы JavaScript, такие как арифметика, сравнение, логика и управление потоком. Это может показаться утомительным, но мы находимся за спиной Ramda.
Но сначала, небольшое введение.
Императивность vs Декларативность
Есть множество различных путей для разделения языков программирования и стилей написания. Это статическая типизация против динамической типизаци, интерпретируемые языки и компилируемые языки, высокоуровневые и низкоуровные, и так далее.
Другое подобное разделение заключается в императивном програмировании против декларативного.
Без погружения вглубь этого, императивное программирование — это стиль программирования, в котором программисты говорят компьютеру, что нужно сделать, объясняя ему, как это нужно сделать. Императивное программирование даёт множество конструкций, которые мы используем каждый день: управление потоком (
if
-then
-else
синтаксис и циклы), арифметические операторы (+
, -
, *
, /
), операторы сравнения (===
, >
, <
, и т.д.), и логические операторы (&&
, ||
, !
).Декларативное программирование — это стиль програмирования, в котором програмисты говорят компьютеру, что нужно сделать, объясняя ему, что они хотят. Компьютер далее должен определить, как получить необходимый результат.
Один из классических декларативных языков — это Prolog. В Prolog програма состоит из набора фактов и набора правил вывода. Вы начинаете программу, задавая вопрос, и набор правил вывода Prolog'а использует факты и правила для ответа на ваш вопрос.
Функциональное программирование рассматривается как подмножество декларативного програмирования. В функциональной программе, мы объявляем функции и далее объясняем компьютеру что мы хотим сделать, совмещая данные функции.
Даже в декларативных программах необходимо выполнять подобные задачи, которые мы выполняем в императивных программах. Управление потоком, арифметика, сравнения и логика всё ещё являются базовыми строительными блоками, с которыми мы должны работать. Но нам необходимо найти способы для выражения этих конструкций в декларативном стиле.
Декларативные заменители
Поскольку мы программируем на JavaScript, императивном языке, это нормально — использовать стандартные императивные конструкции при написании «нормального» JavaScript кода.
Но когда мы пишем функциональные трансформации, используя конвееры и подобные им конструкции, императивные конструкции перестают вписываться с создаваемую структуру кода.
Посмотрим на некоторые базовые строительные блоки, которые предоставляет Ramda для того чтобы помочь нам выйти из этой неприятной ситуации.
Арифметика
Во второй части мы реализовали серию арифметических трансформаций для демонстрации конвеера:
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, subtract, multiply и divide для использования вместо стандартных арифметических операций. Так что мы можем использовать рамдовскую
multiply
там, где мы использовали самописную функцию, мы можем взять преимущество каррированной функции add
для замены нашей addOne
, и мы также можем написать square
с помощью multiply
.const square = x => multiply(x, x)
const operate = pipe(
multiply,
add(1),
square
)
add(1)
очень похожа на оператор инкрементирования (++
), но оператор инкрементирования изменяет переменную, так что он вызывает мутацию. Как мы узнали из первой части, иммутабельность — это основной принцип функционального программирования, так что мы не хотим использовать ++
или его кузена --
.Мы можем использовать
add(1)
и subtract(1)
для увеличения и уменьшения, но так как эти две операции такие распространённые, Ramda предоставляет inc и dec вместо них.Так что мы можем ещё немного упростить наш конвеер:
const square = x => multiply(x, x)
const operate = pipe(
multiply,
inc,
square
)
subtract
является заменой бинарного оператора -
, но у нас ещё имеется унарный оператор -
для отрицания значения. Мы также можем использовать multiply(-1)
, но Ramda предоставляет функцию negate для выполнения этой задачи.Сравнение
Также во второй части мы написали несколько функций для определения, является ли персона имеющей право на голосование. Конечная версия того кода выглядела следующим образом:
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 вместо
===
и gte вместо >=
.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 для
>
, lt для <
и lte для <=
Обратите внимание, что эти функции, как кажется, принимают свои аргументы в нормальном порядке (первый аргумент больше второго?). Это имеет смысл, когда мы используем их в изоляции, но может сбивать с толку при объединении функций. Эти функции нарушают принцип «данные идут последними», так что нам нужно быть осторожными, когда мы используем их в наших конвеерах и подобных им ситуациях. И именно здесь flip и заполнитель (__) могут принести пользу.
В дополнение к
equals
есть ещё identical для определения, являются ли два значения ссылками на то же пространство в памяти.Существует набор случаев основных применений для
===
: проверка, что строка или массив являются пустыми (str === ''
или arr.length === 0
) и проверка, является ли переменная равной null
или undefined
. Ramda предоставляет удобные функции для обоих случаев: isEmpty и isNil.Логика
Во второй части (и чуть выше), мы использовали функции
both
и either
вместо операторов &&
и ||
. Мы также говорили о complement
для мест с !
.Эти комбинированные функции работают прекрасно, когда функции объединяют операцию над тем же значением. Написанные выше
wasBornInCountry
, wasNaturalized
и isOver18
все применялись к объекту персоны.Но иногда нам нужно применить
&&
, ||
и !
к различным значениям. Для подбоных случаев Ramda предоставляет нам функции and, or и not. Я думаю следующим образом: and
, or
и not
работают со значениями, в то время как both
, either
и complement
работают с функциями.В основном,
||
используется для получения значений по умолчанию. К примеру, мы можем написать что-нибудь вроде этого:const lineWidth = settings.lineWidth || 80
Это распространённая идиома, и чаще всего работающая, но полагающаяся на JavaScript логику определения «ложности». Что если
0
является валидным параметром? Так как 0
является ложным значением, мы получим значение линии равное 80.Мы можем использовать функцию
isNil
, о которой мы только что узнали выше, но Ramda снова имеет более логичный вариант для нас: defaultTo.const lineWidth = defaultTo(80, settings.lineWidth)
defaultTo
проверяет второй аргумент на isNil
. Если проверка провалилась он вернёт полученное значение, в ином случае вернёт первый аргумент, переданный ей.Условия
Управление потоком выполнения менее важно в функциональном программировании, но иногда оказывается нужным. Коллекция итерирующих функций, о которых мы говорили в первой части, заботится о большинстве ситуаций с циклами, но условия всё ещё довольно важны.
ifElse
Давайте напишем функцию,
forever21
, которая получает год и возвращает следующий. Но, как нам указывает её имя, начиная с 21 года, он будет оставаться в этом значении.const forever21 = age => age >= 21 ? 21 : age + 1
Обратите внимание, что наше условие (
age >= 21
) и вторая ветвь (age + 1
) могут быть обе написаны как функции age
. Мы можем переписать первую ветвь (21
) как функцию-константу (() => 21
). Теперь у нас будет три функции, которые принимают (или игнорируют) age
.Теперь мы на позиции, когда мы можем использовать функцию ifElse из 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.
const forever21 = age => ifElse(gte(__, 21), always(21), inc)(age)
Ramda также предоставляет T и F в качестве дальнейших сокращений для
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:
const alwaysDrivingAge = age => ifElse(lt(__, 16), always(16), identity)(age)
identity
может принять больше одного аргумента, но всегда вернёт только первый. Если мы хотим вернуть что-то другое, отличное от первого аргумента, для этого существует более общая функция nthArg. Это гораздо менее распространённая ситуация, чем использование identity
.«when» и «unless»
Выражение
ifElse
, в котором одна из логических ветвей является тождественностью, также является типичным патерном, так что Ramda предоставляет нам больше сокращающих методов.Если, как в нашем случае, вторая ветвь является тождественностью, мы можем использовать when вместо
ifElse
:const alwaysDrivingAge = age => when(lt(__, 16), always(16))(age)
Если первая ветвь условия является тождественностью, мы можем использовать unless. Если мы перевернём наше условие на использование
gte(__, 16)
, мы можем использовать unless
.const alwaysDrivingAge = age => unless(gte(__, 16), always(16))(age)
Cond
Ramda также предоставляет функцию cond, которая может заменить выражение
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 предоставляем нам инструменты для того чтобы привести всё это к более чистому виду. Следующий пост, "Бесточечная нотация" рассматривает способы, позволяющие упростить функции, следующие подобному паттерну.