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

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

А можно какой-нибудь пример использования pipeline operator c async / await из реальной жизни?


С синхронным вариантом все понятно: его можно использовать для преобразования коллекций.
Например, написать map, filter, reduce для Object, Map, Set без использования Lodash.


А что насчет асинхронного?

А можно какой-нибудь пример использования pipeline operator c async / await из реальной жизни?

Лично мне кажется что многие привычные асинхронные сценарии в генераторах redux-saga будут выглядеть намного удобнее с пайплайном (пока что запись саг доступна только через Smart-пайплайны, так как в F#-like пайплайнах нет yield). Как и любая запись сценария в функциональном стиле будет удобней, чем в императивном (с моей точки зрения, само собой).

Вот сейчас у меня перед глазами функция без всяких redux-saga, которая содержит сценарий отправки отзыва, она должна сделать три асинхронные операции: если пользователь аноним, то создаётся пользователь, затем собственно отправляется отзыв, затем перезапрашиваются отзывы к текущей странице. Между этими тремя вызовами есть синхронные операции, и было бы неудобно часть действий писать в пайплайне, но иногда разрывать цепочку, чтобы использовать await. В конце концов надо ещё вернуть промис всей этой катавасии, и функциональная запись пайплайна тут как нельзя кстати: вся функция выглядела бы как несколько шагов пайплайна.

Если вы хотите больше точек зрения, то вот тут идут баталии по вашему вопросу: github.com/tc39/proposal-pipeline-operator/issues/86

Я обычно очень положительно отношусь ко всем нововведениям в JS, но новый причудливый оператор для очень узкого юзкейса вместо улучшения стандартной библиотеки кажется мне сомнительным.

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


// Before:
class Comment extends Sharable(Editable(Model)) {
  // ...
}
// After:
class Comment extends Model |> Editable |> Sharable {
  // ...
}

Дальше — больше.
Да, в случае классов нам вполне можно использовать и декораторы для примешивания, но для функциональных компонентов так просто всё не получится (декораторы не применимы просто к функциям). Поэтому обычно в recompose/recompact используют обычный функциональный compose/pipe (который надо импортить из какой-либо библиотеки). Пайплайн позволяет собирать энхансер (higher-order component) из своих хоков (или рекомпоузовских) используя нативный javascript-синтаксис.


Этот оператор (как и стагнирующее предложение bind-оператора) вводится как раз ради появления нативного синтаксиса для обработки цепочек/пайплайнов, а они очень часто используются, это много юзкейзов. Пакетами, реализующими пайплайны и цепочки, завален весь npm. Это примерно то же самое, как ранее, до появления ключевого слова class в es6, каждый начинающий javascript-ер был обязан сделать свою реализацию "полноценного ООП" в js (и написать об этом на Хабр).

Спасибо за объяснения.


Честно говоря, меня вполне устраивает Sharable(Editable(Model)) (да это даже меньше символов занимает), но это вкусовщина.


Что касается recompose и HOC'ов, то и так было понятно, что пайплайны затеваются почти и исключительно под них, поэтому я и ворчу про узкий юзкейс.


Цепочек я не писал уже очень давно, а когда писал, то мне вполне хватало Array#reduce.


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

Интересно мнение сообщества. Какой из предложенных вариантов вам больше нравится?
// С именованием переменных
function getRandomColor() {
  const base = Math.random() * 0xFFFFFF;
  const floored = Math.floor(base);
  const hexString = floored.toString(16);
  return `#${hexString}`;
}

// Просто последовательные вызовы
function getRandomColor() {
  return `#${Math.floor(Math.random() * 0xFFFFFF).toString(16)}`;
}

// С пайплайн оператором
function getRandomColor() {
  return Math.random()
    |> base => base * 0xFFFFFF
    |> Math.floor
    |> floored => floored.toString(16)
    |> hex => `#${hex}`
}

На мой взгляд третий… и первый. Это сильно зависит от конкретного кода. Чем сложнее и замысловатее задача, тем сложнее вам будет написать первый вариант, но проще третий. По сути они идентичны, но в третьем:


  • код выровнен, а оттого проще воспринимается
  • за '|>' цепляется глаз, проще воспринимать саму последовательность действий
  • нет проблемы с именованием

Собственно по этой причине те, кто часто имеет дело с чем-то около-функциональным и не только, предпочитают использовать цепочки трансформаций. Даже без '|>' они позволяют многое упростить и избежать дубляжа. Но, без '|>' — шаг влево, шаг в право — цепочка порвалась.

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


// (1) Просто вызов функций с промежуточными значениями
const doEverything = val => {
    const junk1 = doSomething(val);
    const junk2 = doSomethingElse(junk1);
    return doSomeMore(junk2);
};

// (2) Вложенные вызовы тех же функций
const doEverything = val => doSomething(doSomethingElse(doSomeMore(val)));

// (3) Бесточечный подход (с импортом pipe откуда-нибудь)
const doEverything = pipe(
    doSomething,
    doSomethingElse,
    doSomeMore,
);

// (4) Новый модный молодёжный оператор
const doEverything = val => val
    |> doSomething
    |> doSomethingElse
    |> doSomeMore

Собственно самые читабельные варианты — два последних, но точку останова можно нормально (без доп ухищрений, без изменения кода, без захода в блок извне) поставить только в первом и в последнем.


Получается, что pipeline operator — это самый удобный для чтения и для отладки инструмент из тех, которые на текущий момент существуют в языке.

Опечатка: в примере (2) нарушен порядок вызовов в теле doEverything, должен быть обратный.

Первый. Можно поставить брейкпойнт и отследить что получается на каждом шаге.

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

Кстати в переводе (как и в оригинале) упоминалось частичное применение, правда без ссылки. Предложение proposal-partial-application находится в stage-1, как и сам пайплайн оператор. Так что предполагается, что в светлом будущем они либо вместе выйдут в свет, либо один за другим.


А что за особый образ реализации стандартной библиотеки в F#? Адаптированность к функциональному применению, а-ля ramda?

В пайпах предлагают # в качестве плейсхолдера, в частичном применении — ?. Надеюсь, они все-таки договорятся!


По поводу стандартной библиотеки — у методов так подбирается сигнатура, чтобы "текущий элемент" был последним аргументом. Так можно указать все нужные аргументы в частичном применении, а последний будет автоматически получен из пайпа:


// хорошо
let good a b current = ...
someValue |> good 1 2

// плохо
let bad current a b  = ...
someValue |> (fun x -> bad x 1 2)

Стоит отметить, что такая "неявная" передача значений имеет смысл только в языке со строгими статическими проверками. В джиесе она превратится в очередные грабли.

Первым делом хочу сказать, что мой коммент не является защитой авторов черновика пайплайн-оператора и не является попыткой противопоставлять JavaScript и F# (или превозносить JS над F#). И да, я не собирался писать портянку такую, она сама.


… итого вы аппелируете к:
1) пайплайны
2) частичное применение
3) "особым образом подготовленная стандартная библиотека"
4) статическая типизация


В пайпах предлагают # в качестве плейсхолдера, в частичном применении —?.. Надеюсь, они все-таки договорятся!

Это касается только Smart-пайплайнов. В предложении, которое называют F#-пайплайны, нет дополнительного символа, т.е. любые функции, которые уже каррированы, будут работать с F#-пайплайнами из коробки.


По поводу стандартной библиотеки — исторически так сложилось, что в JS нет стандартной библиотеки, зато есть россыпь всевозможных библиотек, и некоторые из них занимают нишу стандартной библиотеки. Например core-js (спасибо rock) стала частью Babel ещё когда эта штука называлась 6to5. А Ramda.js де-факто стала таким же стандартом при функциональном подходе в JS.


… у методов так подбирается сигнатура, чтобы «текущий элемент» был последним аргументом. Так можно указать все нужные аргументы в частичном применении, а последний будет автоматически получен из пайпа

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


import { map, filter, sum } from 'ramda'

const someValue = [1, 2, 3, 4]
const doubler = x => x * 2

map(doubler, someValue)  // вернёт [2, 4, 6, 8]
map(doubler)(someValue)  // вернёт [2, 4, 6, 8]
someValue |> map(doubler)  // вернёт [2, 4, 6, 8]

// вернёт число 14
someValue
    |> map(doubler)
    |> filter(x => x > 5)
    |> sum

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


Стоит отметить, что такая "неявная" передача значений имеет смысл только в языке со строгими статическими проверками. В джиесе она превратится в очередные грабли.

На это пока нечего ответить, могу лишь подтвердить, хоть я и не использовал ramda в чистом JS (только c FlowType). С flowtype мы получаем сильную типизацию и статические проверки… но другие грабли — настройка и интеграция самого flowtype (и либдефов). Ну и детские баги flowtype, часто — молчаливая потеря обратной совместимости между версиями.

Спасибо за подробный ответ! Со всем согласен, кроме двух маленьких моментов.


Во-первых, RamdaJS и CoreJS все-таки не стандартные, а просто популярные библиотеки. Стандартной библиотекой в моем понимании являются встроенные типы и их методы — String, Array, Number и прочие. В этом смысле несогласованность между частями стандарта остается, хотя лучшего решения я предложить не могу.


Во-вторых, подход Ramda удобен для использования методов из их библиотеки, но из-за отсутствия поддержки каррирования в языке неудобен для написания собственных — приходится использовать костыльный декоратор.

Конечно, не могу не согласиться, что то же самое на уровне языка, а не на уровне пользовательских библиотек, занявших нишу стандартной библиотеки (свято место пусто не бывает), будет гораздо вкуснее.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий