Как стать автором
Обновить

Шпаргалка по функциональному программированию

Время на прочтение 23 мин
Количество просмотров 37K
Всего голосов 39: ↑36 и ↓3 +33
Комментарии 18

Комментарии 18

// оригинальная цепочка вызовов
one(two(three(x)))

// более естественно с точки зрения чтения
pipe(three, two, one)(x)

// более естественно с точки зрения записи
compose(one, two, three)(x)


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


// развёрнуто ход выполнения
const a = one(x);
const b = two(a);
const c1 = three(b);

// аналог без композиции/конвейера
const c2 = three(two(one(x)));

/* более естественно с точки зрения чтения
 т.к. с лева на право функции которые применяем в начале
 и результат вызова которых передаё следующей функции */
const c3 = pipe(one, two, three)(x)

// более естественно с точки зрения записи
// т.к. повторяем запись аналога без композиции/конвейера
const c4 = compose(three, two, one)(x)
Верное замечание, исправил так, чтобы названия были по порядку. Спасибо!

Однако ниже по тексту у вас есть красивая композиция:


const sortedUniqueWordsFromString = compose(sort, unique, words)

// всё-же есть желание уточнить название функции words,
// поскольку без типов в js непонятно из чего слова извлекаем
const sortedUniqueWordsFromString = compose(sort, unique, wordsFromString)

Она хорошо читаеться именно в таком порядке. Попытка выразить в виде:


const getSortedUniqueWordsFromString = pipe(
         getWordsFromString, selectUniqueItems, sort)

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


Это заставляет меня задуматься над вопросами:


  1. может вариант one(tow(three(x))) — был лучше в каком-то ином смысле?
  2. может ли более декларативный вариант использоваться более широко?

Второй вопрос уточню комментарием ниже, который будет к пункту “Создание новых абстракций”

НЛО прилетело и опубликовало эту надпись здесь

Хорошая шпаргалка. Действительно, это и есть ФП, а не всякие там монады.

НЛО прилетело и опубликовало эту надпись здесь

Как проверить случайность компилятором с помощью типов?

НЛО прилетело и опубликовало эту надпись здесь

Разница между конвейером и композицией не в направлении потока вычислений — он и там и там может идти в любую сторону; скажем, в F# есть композиционные >> и << и конвейерные <| и |>. Разница в группировке этих вычислений. При композиции несколько функций склеиваются в одну, и потом в итоговую может скармливаться аргумент. В случае конвейера первым делом берётся аргумент и строго последовательно преобразуется каждой из функций. Композиция возможна без аргумента (результат — самодостаточная функция), конвейер — нет. Композицию можно группировать с произвольной ассоциативностью: ((foo ∘ bar) ∘ baz) ∘ qux) эквивалентно foo ∘ (bar ∘ (baz ∘ qux)), (foo ∘ bar) ∘ (baz ∘ qux) или foo ∘ (bar ∘ baz) ∘ qux — это полезно при рефакторинге, можно выделять куски в отдельные функции. Конвейер перегруппирован быть не может, ((x |> foo) |> bar) |> baz строго левоассоциативно.

Композиция возможна без аргумента (результат — самодостаточная функция), конвейер — нет.

Мне кажется слово "конвеер" (pipeline) уже перегружено разными смыслами, поэтому всегда стоит уточнять, что конкретно имеется ввиду.


Функции compose и pipe, описанные в статье, это функции. Они склеивают несколько функций в одну (composition). Их названия — уже сложившийся жаргон в JavaScript. Разница между ними лишь в порядке аргументов, которая влияет исключительно на читабельность (суть — вкусовщина).


Оператор |> из F# это частный случай применения функции (application). По сути это способ записать вызов функции задом наперед (где на первом месте стоит аргумент, а функция — за ним):


x |> foo  // эквивалентно foo x

В JS такого оператора нет, но можно придумать подобную функцию:


const applyTo = x => f => f(x)
applyTo(42)(console.log)
// 42

Кстати, у нее есть интересное свойство:


applyTo(42)(applyTo)(applyTo)(applyTo)(console.log)
// 42

Вызывая applyTo(42) мы захватываем значение, а потом извлекаем его с помощью подходящей функции:


const then = f => x => applyTo(f(x))
applyTo(42)(then(x => x + 1))(then(x => x * 2))(console.log) 
// 86

или если поменять порядок аргументов, то мы сможем записать все в строчку без вложенных скобок:


const andThen = x = > f => applyTo(f(x))
applyTo(42)(andThen)(x => x + 1)(andThen)(x => x * 2)(console.log) 
// 86

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

В пункте “Создание новых абстракций” вы по ходу дела меняете стиль именования функций, что ведёт к переходу от декларативного вида к императивному. Ведь функции words, unique, sort — скажем так простые, существительные и глаголы из которых потом можно построить красивую композицию. Всё-же words => wordsFromString. Красивую — в смысле близкую к виду предложения на английском языке.


const sortedUniqueWordsFromString = compose(sort, unique, wordsFromString)

Тогда и именование композитных функций будет другим… в зависимости от того как нам их собрать будет удобнее:


const sortedUniqueItemsFrom = compose(sort, unique);
const sortedUniqueWordsFromString = compose(sortedUniqueItemsFrom, wordsFromString)

// либо в иной группировки функций
const uniqueWordsFromString = compose(unique, wordsFromString)
const sortedUniqueWordsFromString = compose(sort, uniqueWordsFromString)


А если мы вдруг заходим подсчитать количество уникальных слов в строке, то так и напишем:


const amountOf = items => items.length;

const amountOfUniqueWordsFromString = compose(amountOf, unique, wordsFromString);

// либо
const amountOfUniqueItemsInArrayOf = compose(amountOf, unique);
const amountOfUniqueWordsFromString = compose(
        amountOfUniqueItemsInArrayOf, wordsFromString);

// либо
const uniqueWordsFromString = compose(unique, wordsFromString)
const amountOfUniqueWordsFromString = compose(
       amountOf, uniqueWordsFromString)


И вот вопросы:


  1. насколько много можно выразить следуя этому подходу? Другими словами, насколько произвольные задачи/алгоритмы можно таким образом описать?
  2. вышеприведенное — это просто игры с кодом в песочнице, либо оно может быть основой для повышения читаемости продакшн кода?
  3. а что ещё может помочь в стремлении к данному стилю написания кода? Может оборачивание значений и монады?
  4. а есть ли такой момент, когда приходиться отказываться от декларативного описания и переходить на императивное? И если да, то чем он обусловлен? А вместе с этим — как тогда на стыке согласовывать оставаясь поближе к “красивой” читаемости кода?
Статья интересная, но не без помарок. Здесь, например, будет ошибка про ReferenceError:
const foundWords = words(text)
const uniqueWords = unique(wordsFound)
Спасибо за полезную и хорошо структурированную статью! Пожалуй, лучшая статья по ФП из виденных мною.

Функциональный яваскрипт всем хорош, кроме синтаксиса. Можно добавить про такой транспилятор, как LiveScript, который даёт вам недурную иллюзию, что вы пишете на хаскеле. Я небольной генератор сайтов на нём сделал: https://github.com/punund/20ful


В числе прочего, я применил импорт всей рамды в глобальный контекст:


global <<< require 'ramda'

Это позволяет писать без префиксов, особенно если без рамды уже не можешь:


dst = toPairs Compilers.formats
         |> find((.1) >> has src)
         |> prop \0
         |> defaultTo src

Это переводится в


dst = defaultTo(src)(
      prop('0')(
      find(compose$(function(it){
        return it[1];
      }, has(src)))(
      toPairs(Compilers.formats))));

но мне сложно представить, что это легче понимать, чем первый вариант. А без библиотеки этот код был бы не знаю какого размера.

Кроме того, в режиме разработки Immer замораживает все объекты, которые возвращает produce, чтобы защитить разработчика от возможных нечаянных мутаций.

Начиная с 8-й версии уже замораживает и для production версии.

Поправил в тексте, спасибо!
А каким образом результат этого
// better
notifications
    .filter(isOpen)
    .filter(isLang)

коррелирует с результатом этого
// the best
compose(
    isLang,
    isOpen
)(notifications)

?

Судя по первому куску, функции isLang и isOpen, принимают элемент массива и возвращают какой-то результат, а во втором куске функции соединили в одну и в результирующую функцию передали аргументом массив целиком.

Тут либо должно быть

notifications.filter(compose(
    isLang,
    isOpen
))

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