Comments 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)
Выглядит более императивно, т.е. мы больше акцентируем внимание на том что делать, чем на том какой результат мы хотим получить.
Это заставляет меня задуматься над вопросами:
- может вариант one(tow(three(x))) — был лучше в каком-то ином смысле?
- может ли более декларативный вариант использоваться более широко?
Второй вопрос уточню комментарием ниже, который будет к пункту “Создание новых абстракций”
Хорошая шпаргалка. Действительно, это и есть ФП, а не всякие там монады.
Разница между конвейером и композицией не в направлении потока вычислений — он и там и там может идти в любую сторону; скажем, в 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)
И вот вопросы:
- насколько много можно выразить следуя этому подходу? Другими словами, насколько произвольные задачи/алгоритмы можно таким образом описать?
- вышеприведенное — это просто игры с кодом в песочнице, либо оно может быть основой для повышения читаемости продакшн кода?
- а что ещё может помочь в стремлении к данному стилю написания кода? Может оборачивание значений и монады?
- а есть ли такой момент, когда приходиться отказываться от декларативного описания и переходить на императивное? И если да, то чем он обусловлен? А вместе с этим — как тогда на стыке согласовывать оставаясь поближе к “красивой” читаемости кода?
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 не просто склеивает несколько функций в одну, но и добавляет неочевидное поведение в результирующую функцию, в данном случае в виде фильтрации элементов переданного аргументом массива.
Шпаргалка по функциональному программированию