Руководство по функциональному программированию для любителей эмодзи

Превратите грозовые облака в солнечные облака

Расширенное использование карты массива, уменьшения массива и функциональной композиции с помощью конвейера

Изучите функциональное программирование с помощью эмодзи и JavaScript. Примеры кода должны быть достаточно простыми, чтобы их можно было понять без каких-либо предварительных знаний, но я могу представить, что это будет выглядеть немного странно. Кроме того, JavaScript фактически не поддерживает смайлики в качестве имен переменных JavaScript. По этой причине эти примеры кода не будут работать без изменений.

Начнем с наших методов add и subtract из прошлого раза:

add = additive => item => item + additive
subtract = subtractor => item => item - subtractor

Мы говорили об использовании нескольких операторов map, чтобы превратить наши грозовые облака в солнечные:

[⛅, 🌦️] = (
    [🌩️, ⛈️️]
    .map(subtract(⚡))
    .map(add(☀️))
)

Но знаете ли вы, что мы можем удалить map, составив сложение и вычитание:

addSun = add(☀️)
removeLightning = subtract(⚡)
[⛅, 🌦️] = (
    [🌩️, ⛈️️]
    .map(item => (
        removeLightning(
            addSun(item)
        )
    )
)

Зачем нам это делать? Мы могли бы просто сохранить наши два map утверждения, и все были бы счастливы! Обычно это именно то, что вы делаете, но в некоторых случаях вам может потребоваться другой способ цепочки с использованием compose или pipe. Они выполняют те же функции, за исключением того, что pipe принимает параметры в обратном порядке, как в польской нотации:

compose = (second, first, item) => second(first(item))
pipe = (item, first, second) => second(first(item))

Обычно эти функции возвращают, а не принимают item напрямую:

compose = (second, first) => item => second(first(item))
pipe = (first, second) => item => second(first(item))

Мы будем использовать pipe для наших примеров, поскольку он упрощает чтение слева направо.

Предположим, нам нужно микрооптимизировать наш код, а две карты - это слишком много. Если мы хотим свести все к одной карте, нам нужно скомпоновать наши функции. Как мы видели в предыдущем примере, это может быстро стать некрасивым! Написав упрощенную pipe функцию, мы можем достичь нашей цели и сохранить код чистым:

pipe = (first, second) => item => second(first(item))
[⛅, 🌦️] = [🌩️, ⛈️️].map(pipe(subtract(⚡), add(☀️)))

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

Функционально это просто использовать reduce, но, не понимая основ, довольно сложно рассуждать. Давайте посмотрим на это процедурно:

item = null
updateItem = change => {
    item = change(item)
}
pipe = (...functions) => {
    [item] = functions.splice(0, 1)
    
    for(let i = 0, l = functions.length; i < l; i++) {
        updateItem(functions[i])
    }
    
    return item
}
⛅ = pipe(☁️, subtract(⚡), add(☀️))

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

Первое, что мы делаем в нашем процедурном pipe, - это удаляем первый аргумент. Это будет наш item. Мы будем зацикливать остальные функции, вызывая их одну за другой и обновляя item до последнего значения, пока мы не закончим цикл, в котором мы вернем измененное item. Довольно грязно.

В этом коде много мутаций. Мы создаем item, а затем изменяем значение item каждый раз, когда зацикливаемся где-то еще в коде, и updateItem создает побочный эффект.

Здесь есть еще одна проблема: splice. Эта функция изменяет массив; вытаскивая набор значений и оставляя исходный массив с оставшимися значениями. Конечно, это имеет смысл прямо сейчас, когда код небольшой, но по мере того, как вы добавляете больше частей, будет намного сложнее определить состояние item и массива functions.

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

Вот то же pipe с использованием неизменности, функциональной композиции и reduce.

pipeReducer = (item, change) => change(item)
pipe = (...functions) => {
    [startingItem] = functions
    
    return (
        functions
        .slice(1)
        .reduce(pipeReducer, startingItem)
    )
}
⛅ = pipe(🌩️, subtract(⚡), add(☀️))

Обратите внимание, как мы используем slice (неизменяемую версию splice), поэтому нам не нужно изменять наш массив функций. Самое первое, что мы делаем, это используем деконструкцию, чтобы вытащить startingItem из functions. Затем мы снова используем slice, на этот раз, чтобы взять все, кроме первого элемента. Затем мы можем запустить reduce в нашем меньшем массиве и вызвать change(item) для одного возвращаемого значения за другим.

В то время как reduce перебирает каждое значение в массиве, например map, каждая итерация получает как предыдущее значение, так и текущее. Основное отличие состоит в том, что в конечном итоге он возвращает одно значение любого типа. Кроме того, если вы не укажете reduce начальное значение, по умолчанию будет использоваться первое значение в массиве. Это означает, что мы можем упростить это еще больше:

pipeReducer = (item, change) => change(item)
pipe = (...functions) => functions.reduce(pipeReducer)
⛅ = pipe(🌩️, subtract(⚡), add(☀️))

Теперь это выглядит действительно хорошо! Как и add и subtract, мы хотим использовать замыкания, чтобы pipe могли создавать новые функции. Есть несколько способов написать pipe. Один из них - явно передать наш начальный элемент в качестве начального значения для reduce:

pipe = (...functions) => startingItem => (
    functions
    .reduce(pipeReducer, startingItem)
)

Другой способ - добавить наш startingItem в массив и concat наши конвейерные функции:

pipe = (...functions) => startingItem => (
    [startingItem]
    .concat(functions)
    .reduce(pipeReducer)
)

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

changeStormyToSunny = pipe(subtract(⚡), add(☀️))
⛅ = changeStormyToSunny(☁️)

Важным преимуществом является то, что мы неожиданно получили многоразовый код изменения погоды! Используя наши знания о замыканиях, генераторах функций и pipe, мы можем преобразовать двойное map в одно читаемое map.

add = additive => item => item + additive
subtract = subtractor => item => item - subtractor
pipeReducer = (item, change) => change(item)
pipe = (...functions) => startingItem => (
    functions
    .reduce(pipeReducer, startingItem)
)
changeStormyToSunny = pipe(subtract(⚡), add(☀️))
[⛅, 🌦️] = [🌩️, ⛈️️].map(changeStormyToSunny)

ПОЧУВСТВУЙТЕ ПРОСТОТУ!

Щелкните здесь, чтобы перейти к Части 3!

Больше Чтений

Если вас интересуют другие темы, связанные с функциональным программированием, вам следует ознакомиться с другими моими статьями: