Комментарии 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 — это самый удобный для чтения и для отладки инструмент из тех, которые на текущий момент существуют в языке.
Первый. Можно поставить брейкпойнт и отследить что получается на каждом шаге.
Кстати в переводе (как и в оригинале) упоминалось частичное применение, правда без ссылки. Предложение 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 удобен для использования методов из их библиотеки, но из-за отсутствия поддержки каррирования в языке неудобен для написания собственных — приходится использовать костыльный декоратор.
Приключения оператора pipeline в babel@7