Comments 115
async/await
как раз в том, чтобы сделать асинхронный код максимально похожим на обычный, чтобы его было проще понимать и легче отлаживать.Мы не определяли промежуточные переменные для хранения этого состоянияИногда это бывает очень нужно, например для такого случая:
const a = await getA();
const b = await getB(a);
const c = await getC(a, b);
Аналогичный код с promise'ами выглядит очень гадко.
У меня async/await был самой ожидаемой фичей ES-Next, пока не пришлось столкнуться с Observables :) Теперь async/await, в общем-то, не у дел, но это и не значит, что он плох.
Ну и, к слову, несмотря на то, что код выглядит как синхронный, он именно что только «выглядит» так — асинхронность все равно надо держать в уме, и, поскольку await разворачивается в Promise, все равно нужно понимать, как именно он это делает.
Promise.try(() => getA())
.then(a => getB(a)
.then(b => getC(a, b))
);
Ну… но я привы… ну ла… Не, стоп, но зато явная асинхронность и можно одновременно посылать несколько запросов + отменять их (с тем же bluebird).
Ну то есть ваш пример вырожден. Добавьте ту же обработку ошибок, и у дела уже хуже.
async function fetchAll(urls) {
var results = [];
for (let url of urls) results.push(await fetch(url));
return results;
}
Даже проще.
function fetchAll(urls) {
cosnt results = [];
return urls.reduce(
(promise, url) => promise.then(() => fetch(url)).then(user => results.push(user)),
Promise.resolve()
).then(() => results);
}
А еще вы в курсе, что ваш код отсылает запросы последовательно, один за одним?
А если нужно параллельно?
function fetchAll(urls) {
return Promise.all(urls.map(url => fetch(url)));
}
а на промисах — раз и готово
async function fetchAll(urls) {
return urls.asParalell().select(url => await fetch(url));
}
А что такое asParallel()
?
В формулировке "проще", я имел в виду, что не нужно подключать Babel или колдовать с флагами в Node.js.
Если async функции будут поддерживаться без дополнительных проблем, то мой вариант конечно же не проще.
Круто. Притащили rx.
function fetchAll(urls) { cosnt results = []; return urls.reduce( (promise, url) => promise.then(() => fetch(url)).then(user => results.push(user)), Promise.resolve() ).then(() => results); }
Почему вы считаете, что такой код проще такого? Зачем reduce, который усложняет понимание кода?
function fetchAll(urls) {
const results = [];
let promise = Promise.resolve();
for (let url of urls) {
promise = promise
.then(() => fetch(url))
.then(user => results.push(user))
}
return promise.then(() => results);
}
Кому-то больше нравится писать код с for-циклами, а кто-то больше любит методы массива.
Тут уже личный выбор каждого. Я предпочитаю, не бегать по массиву через for, а использовать filter/map/reduce, но это мое личное предпочтение.
async/await — просто синтаксичский сахар для Promise, не вижу причин спорить
Вроде же изначально договорились никакие библиотеки не использовать.
Так-то я могу тоже взять гипотетическую библиотеку url-fetcher
, которая делает то, что мне нужно, надо лишь ее вызвать.
fetchUrls(urls);
Вы видимо тайком договаривались. :-)
В том-то и дело, то атомы не "делают то, что вам нужно". Они лишь позволяют синхронный код исполнять асинхронно:
printTitlesParallel( urls ) {
for( let text of this.fetchAllPrallel( urls ) ) {
this.log( this.parseHTML( text ).title )
}
}
printTitlesSerial( urls ) {
for( let text of this.fetchAllSerial( urls ) ) {
this.log( this.parseHTML( text ).title )
}
}
Не спорю, что с атомами, как и с rx, async.js и другими библиотеками код будет короче чем без них.
Но вообще это комментарии к статье о том, нужен ли async/await в стандарте языка. Библиотеки здесь не при чем.
Даже проще.
Но есть нюанс: вызов urls.reduce
приведёт к созданию огромного объекта
Promise.resolve().then(...).then(...).then(...).then(...).then(...).then(...)....
,
тогда как код с async/await будет генерить промисы по мере необходимости.
В реальной ситуации это, конечно, не играет никакого значения — вряд ли вы будете по 100000 промисов отрабатывать за раз.
А чем именно плоха большая цепочка then?
Зато можно быть уверенным, что пройдутся все урлы, а не потеряется что-то в процессе, если кто-то снаружи смутирует массив urls
. Вот пример. Очень невероятный пример, но все же.
Точно так же можно смутировать и элементы массива urls
, если они представляют собой объекты, не строчки. Ещё более невероятная ситация, но тоже возможна.
Хорошо, а что с ответом на вопрос:
А чем именно плоха большая цепочка then?
Т.Е. у нас есть модуль обновления модели:
for (var model of neededModels) {
model.load();
}
А также есть модуль (никак не связанный с загрузкой моделей), который отвечает за выполнение какого-либо действия:
onModelLoad(models) {
if (this.allModelsReady(models)) {
this.doMyAction();
}
}
Мы к этому подходу пришли задолго до бума промисов и опробовали его на огромной игре — когда отказались от прямой подписки на события («загрузи и сделай это»), то сложность приложения значительно снизилась, а «колбек-хел» забылся как страшный сон.
Потому не могу понять, почему с ними до сих пор носятся так сильно.
Чем плоха большая цепочка then? Да потому что что угодно может произойти, пока она выполняется. А еще нельзя показать промежуточный уже загруженный результат. А ещё нельзя не выполнить колбек (если пользователь ушел с этого экрана). И вообще ад колбеков никуда не девается.
Просто создаётся слишком тяжёлый объект, но я это проблемой не считаю.
Вот более сложный пример для перевода в промисы:
function fetchUntilAvailable(url) {
while (true) {
let result = await fetch(url);
if (isGood(result))
return result;
else
await delay(timeOut);
}
}
const fetchUntilAvailable = (url) =>
fetch(url)
.then(result => isGood(result)
? result
: new Promise(resolve => setTimeout(resolve, timeOut))
.then(() => fetchUntilAvailable(uri)))
Вообще плохо что я повелся и написал это так, ваша функция грязная и пахнет =(
Из контекста приходят: timeOut, delay (зачем брать из контекста delay и из него же timeOut?), isGood. Тестировать такую функцию почти невозможно =/
Плевать на нечитаемость! Зато другие хипстеры на афтерпати будут восхищены!
Да успокойся ты, глупый хипстерофоб. Я же приписал в конце — что функция убогая и не тестируемая сама по себе, так что я ошибся когда вообще попытался написать ее "красиво"
А если серьезно, то раз так сложно в таком стиле написать хорошо, то может не выпендриваться и просто писать хороший код в классическом стиле, а не стараться угнаться за сиюминутной модой?
Давайте так, я пытался и у меня не получилось, ок?
function fetchUntilAvailable(url, retryTimeout) {
return fetch(url).then(result => {
if(!isGood(result)) {
return delay(retryTimeout).then(() => fetchUntilAvailable(url, retryTimeout));
}
return result;
});
}
Согласен, время timeout можно передать в аргументах. А в утилитарной функции delay не вижу ничего плохого. Удобнее, чем каждый раз new Promise разворачивать
(зачем брать из контекста delay и из него же timeOut?)
delay — библиотечная функция, timeOut — константа в проекте. А как иначе?
Ваш код стремится к нечитаемости
function fetchUntilAvailable(url) {
while (true) {
let result = await fetch(url);
if (isGood(result))
return result;
await delay(timeOut);
}
}
//А насчет промисов тоже решение имеется:
function fetchUntilAvailable(url) {
return fetch(url).then(function(result) {
return isGood(result) && result
|| delay(timeOut).then(() => fetchUntilAvailable(uri)));
});
}
//при условии, что delay возвращает Promise
Хотя Ваш вариант (await) мне нравится куда больше.
fucntion fetchAll(urls) {
return Promise.all(urls.map(fetchAsync));
}
Но смысл есть, когда количество урлов идет на тысячи.
Ограничивать число одновременных запросов можно на уровне модуля http.
Зачем засорять логику приложения такими подробностями?
Вот видите, уже не так красиво и прозрачно, например вот вы в своем примере ошиблись.
Так и не понял аргументов автора.
"Async/await плохо, потому что это императивно, а значит не модно, а нас ведь считают крутыми, потому что мы пишем на ФП."
Без шуток. Это аргументация профессиональных программистов. И они правда так думают.
При этом используются целых три стиля — flatMap (на сколько я понимаю, это в js называется «через callback»), с помощью for (в js аналога, по моему, нет, похоже на do в Haskell), и async/await.
Для меня с flatMap единственное неудобство — большой уровень вложенности синтаксических конструкций (соответственно проблема, как поступать с отступами). for выглядит чище, я последнее время склоняюсь к нему. Стиль async/await имеет много фанатов, но мне от показался малопонятным (я давно программирую на функциональных языках и более императивный код мне читать сложнее). Кроме того, в этом коде очень легко где-нибудь забыть написать await на какую-нибудь фьючу, результат которой игнорируется (в нашем случае это обычно было удаление созданных для теста сущностей), и получить race condition.
В общем, async/await мне не понравился.
- промисы не решали в понятном виде большое количество проблем. Например — использование while или switch-case внутри логики (а особенно все вместе).
- правильно воспринимать их не как сахар, а как специфическую инструкцию, которая делает 2 отдельных вещи: уступает поток исполнения и ожидает выполнения результата. Мы можем использовать ее и для того, и для другого.
- async/await действительно является шагом назад под давлением внешних факторов, но не потому что она не функциональная, а потому что реализует подмножество функционала функций-генераторов, и не имеет особого смысла (кроме возможностей для подсветки и простоты самой по себе). Я бы предпочел видеть синтаксис вроде Promise.generator(function() {}), он бы дал куда больше гибкости и возможностей для тестирования — как минимум за счет инструкции yield.
- то, что вы любите функциональное программирование, не значит, что его любят все. JS пытается стать универсальным языком, а не вторым OCaml.
Изучаем новые слова, такие как функторы, монады, моноиды и вдруг наши dev-друзья начинают считать нас крутыми, потому что мы говорим эти странные слова довольно часто!
Скорее не крутыми, а смешными.
Ведь мы писали обычный императивный код, как в 80-х годах, обрабатывали ошибки с try/catch, как в 90-х годах
О нет! Если я напишу более читаемый и поддерживаемый код, то современные хипстеры меня засмеют, ибо это уже не модно и вообще, так писали в 90-х, какой позор!
Статью писал сектант. «Не юзайте асинк-евейт, т.к. это богомерзкая императивщина, а свидетели ФП не используют ничего из мира императивщины, даже если это значительно удешевит поддержку кода». Фу.
И снова не крутые, а смешные, как и обычно.
Кстати, ваш священный Редакс тоже написан в императивном стиле. Видимо, чтобы продавать веру простому люду не обязательно самому в это верить:
function subscribe(listener) { if (typeof listener !== 'function') { throw new Error('Expected listener to be a function.') } let isSubscribed = true // мутабельность ensureCanMutateNextListeners() // нет чистоты nextListeners.push(listener) // мутабельность, нет чистоты return function unsubscribe() { if (!isSubscribed) { // нет чистоты return } isSubscribed = false // мутабельность, нет чистоты ensureCanMutateNextListeners() // нет чистоты const index = nextListeners.indexOf(listener) // нет чистоты nextListeners.splice(index, 1) // мутабельность, нет чистоты } }
О нет! Если я напишу более читаемый и поддерживаемый код, то современные хипстеры меня засмеют, ибо это уже не модно и вообще, так писали в 90-х, какой позор!Хотите сказать, на ФП не писали в 90х? Или 80х? Ну конечно, ФП же придумали хипстеры, которые вам спать по ночам не дают ;)
О чем вы? Я ведь процитировал предложение из топика по поводу которого была моя ирония. Или вы увидели только то, что хотели?
Процитирую лично для вас, Этот текст написан в статье:
Ведь мы писали обычный императивный код, как в 80-х годах, обрабатывали ошибки с try/catch, как в 90-х годах
Так что ваш вопрос должен быть адресован к автору топика
Если не высмеивать подобные творения, то получим еще одного программиста, который судит категориями «это круто» / «это модно» / «надо отказываться от ооп в пользу фп» а то и начинает тоже писать такие статьи. Если бы наша среда была крайне конкурентной, то может оно было бы и хорошо, но, к сожалению, сейчас очень трудно найти себе в команду хороших программистов.
Вот и тут, хорошим программист станет тогда, когда сам наберется опыта, лично наступив на каждую граблю. Я понимаю вашу усталось от всего этого фанбойства, но с этим ничего не поделать.
сейчас очень трудно найти себе в команду хороших программистов.Горькая правда
Да, педалили, набирались опыта, нас критиковали, на какую-то критику обижались, какая-то была слишком аргументированной, чтобы ее не слышать.
Лично я очень много брал именно из комментариев. Статья на какую-либо тему, а в комментариях в пределах аргументированного спора ты видишь две противоположные мысли. Они между собой спорят, не хотят уступить, не слышат аргументов друг-друга, но ты то когда новичок и читаешь это — не имеет реально сформированного мнения, потому слышишь аргументы одной и другой стороны. С какими-то не согласен, какие-то вообще не понимаешь, т.к. не имеешь опыта, какие-то ошибочные. Но это — яркий способ понять вещи, которые в книжке описано намного хуже.
Вот спорит сторонник фреймворка А и фреймворка Б, каждый говорит, что его лучше, а другой хуже. А ты читаешь и узнаешь, вот, оказывается, если смотреть на фреймворк А под таким углом, то он и ничего вообще-то, а вот эта фича в фреймворке Б оказывается так используется.
А что смешного-то?
Да много чего.
— Смешно то, что «императивный подход — это девяностые, а функциональный — это десятые».
— А еще, что это считается важным аргументом в технической статье.
— Что думают, что пишут на JS в функциональном стиле, а в коде не имеют ни чистоты ни декларативности
— Что используют алгоритм квадратичной сложности, где более читаемый и очевидный алгоритм линейной сложности просто чтобы код казался функциональным.
— Потому что мерилом качества кода выступает не «мы поддерживаем огромный продукт уже 3 года, но все-равно с легкостью и минимум багов вносим в него функционал», а «наши dev-друзья начинают считать нас крутыми, потому что мы говорим эти странные слова довольно часто!»
— Смешна не функциональная парадигма, она очень крута в определенных областях, смешны люди, которые ее продвигают подобными статьями.
Мне кажется, имелось в виду, что явное в данном случае лучше неявного. И если писать асинхронный код, делая вид, что он синхронный, то рано или поздно начнётся танец по граблям.
Программисты C# уже давно пишут асинхронный код с использованием async/await и не испытывают никаких проблем с граблями. Более того, код с использованием async/await получается более производительный, чем основанный на использовании Task.ContinueWith
(аналог JS Promise.then
).
Программисты C# уже давно пишут асинхронный код с использованием async/await и не испытывают никаких проблем с граблями.
С чего Вы так решили? Ещё как испытывают и часть из этих проблем даже официально признана и решается последующими релизами C# 6, C# 7, etc.
В принципе, тут невозможно сделать недырявую абстракцию, т.к. код по факту асинхронный и попытки это замаскировать, прикинувшись, что он как бы такой же как синхронный, ни к чему хорошему не ведут. Просто чем дальше, тем более изощрёнее будут грабли.
С чего Вы так решили? Ещё как испытывают и часть из этих проблем даже официально признана и решается последующими релизами C# 6, C# 7, etc.
На грабли наступают, как мне кажется, те, кто решил заняться асинхронным программированием, не имея опыта многопоточного программирования, где граблей ещё больше. А синхронный многопоточный код в однопоточный асинхронный переводится очень просто и безболезненно.
Что касается C# 6.0: просто ввели возможность написания await в catch и finally блоках, что ещё больше сделало асинхронный код похожим на синхронный. В C# 7.0 существенных изменений больше нет.
В C# 7 — "Generalized async return types", в каком-нибудь C# 8 ещё с out-параметрами накостылят или ещё что-нибудь. В принципе, о том и речь, что асинхронный код, похожий на синхронный, будет всегда плохим решением, потому что по факту он несинхронный. И аргумент, что те, кто хорошо понимает что происходит за сценой, не испытывают проблем — не работает. Потому что вся эта декорация делается не для них, а для тех, кто ещё толком не понимает… типа "не задумывайся, что код больше не синхронный, просто добавь воды async/await".
Generalized async return types в C# я уже пощупал — фигня это, правда, тем более, что они все равно на Task завязаны. Прироста в производительности по сравнению с обычным Task вообще нет — основной оверхед там от state machine идёт. Теоретически, польза от этого есть при полном переписывании штатного TPL на свой, с блекджеком и шлюхами.
Так я и не имел в виду, что будут какие-то действительно полезные или масштабные доработки. Это скорее затыкание дыр в абстракции "асинхронный код, похожий на синхронный", которая дырява by design.
Наоборот. Асинхронный код — костыль для многозадачности в рамках одного потока. В нормальных языках есть потоки и/или сопрограммы. И те и другие — синхронны.
Так и есть. В чём принципиальная разница между сотней асинхронных задач и сотней потоков, ожидающих выполнения? Да ни в чём. Просто механизмы синхронизации на уровне операционной системы дорогие, вот и приходится выкручиваться с помощью кооперативной многозадачности в юзерспейсе.
Просто механизмы синхронизации на уровне операционной системы дорогие, вот и приходится выкручиваться с помощью кооперативной многозадачности в юзерспейсе.Речь не про уровень ОС, а про корутины на уровне программы которая написана синхронным кодом. Дак вот по сравнению с ним, асинхронный код — костыль.
В том, что потоков будет не сотня, а на порядок меньше. Сопрограмм (которые могут жить и на одном потоке) тоже будет на порядок меньше. И только асинхронное программирование требует разбиения бизнес задачи на множество мелких асинхронно исполняющихся технических подзадач вида "обработать ответ от такого-то апи и вызвать следующее".
Асинки не параллельные. Код все равно последовательный, просто код не виснет когда ожидает ответа.
В частности, практически каждый метод имеет побочные эффекты, любой метод может изменить внутреннее состояние аргумента, и, в общем-то, часто именно эти явления и ожидаются. То есть, идеологически полный провал.
Кстати, `isUserValidAsync` было бы лучше назвать assertValidUserAsync или использовать по-другому.
С другой стороны, в случае использования async-await, на мой взгляд, существенно улучшается читабельность кода от чего напрямую зависит его качество и стоимость поддержки. Хотелось бы заметить, что читабельность улучшается за счет меньшего количества всяких скобочек, запятых, вызываемых методов и вообще меньше кода.
При этом никто не запрещает использовать элементы функционального подхода, только без лишних ограничений на способ выражения желаемого. К тому же, наверняка, JS-движок следующей версии сможет существенно оптимальнее представить внутри себя async-await конструкции, чем свалку promise'ов, а программист все так же может продолжать использовать более менее простую модель с выворачиванием этого всего в promise'ы.
function doSomeWork(request) {
// код...
// где в середине
if (request == null) {
return;
}
// где-то в конце, doPost возвращает Promise.
return request.doPost(/**...*/);
}
из-за чего лезут нехорошие баги. Хотя это не проблема Promise, а их некорректное использование.
На счет статьи показалось, что автор поведал нам хронологическую ленту развития JS
Решение выглядело бы примерно так:
Не так, вы забыли позаворачивать все хендлеры в try-catch-и, иначе любая ошибка убьёт процесс.
Мне кажется, что спохватываться надо было гораздо раньше: по сравнению с возможностями библиотеки Async даже промисы выглядят шагом назад.
Дело в том, что у промисов есть метод
То есть переход на промисы выглядит как переход на новый (и ещё не обросший возможностями) фреймворк, ради которого отбрасывается история многих лет эволюции библиотеки Async (начиная от первого коммита
То, что новый фреймворк сделали частью языка, в практическом отношении ничего особенного не меняет. (Кроме того только, что процессу переизобретения велосипедов будет сопутствовать не менее увлекательный процесс подпирания костылями-полифиллами всех тех прежних версий, которые останутся в прежних браузерах.)
Поэтому лучшее, что можно сделать с промисом — это сунуть его
Your callback hell is my home.
Но async-await всё равно удобные. Удачно удалось сократить один модуль на промисах, переписав его на async-await, на 40%, и добавлять в него новый функционал стало проще и приятнее ^__^
То что «фреймворк» делают частью языка меняет абсолютно все. Как минимум, можно быть уверенным что выполнение подобных задач будет оптимизировано на уровне интерпретатора и это положительно скажется на производительности.
PS: и для Promise и для async/await пока без полифилов можно писать только на Node.js и прототипы в хроме канари. Сафари не поддерживает без полифилов fetch API, Edge… нет на Маках, Хрома нет на IOS Но господи как я рад что сейчас не 2009 год и это самые большие проблемы хайпа.
Babel 6, Webpack 2, React 15, ES6+ сделали жизнь на много проще.
В Scala (и scala.js) существуют и промисы (только используется другой нейминг, вместо Promise
они называются Future
) и async/await. И все предпочитают использовать именно промисы (фьючи). Причина в том, что async/await создает ложную иллюзию синхронности происходящего. Это может привести к тому, что синхронная и асинхронная логика будут смешаны вместе и в какой-то момент await потеряется перед асинхронной функцией. Или наоборот, await "на всякий случай" будет приписываться везде, даже если функция не асинхронная. При использовании промисов синхронные и асинхронные потоки явно отделены друг от друга. Глазами сразу видно, где тут у нас асинхронное выполнение, а где синхронное. И это потенциально более безопасно и не приведет к ошибкам, которые я выше описал.
Чтобы асинхронный код не превращался в кашу в Scala используется for-comprehension. И такой асинхронный код выглядит почти как синхронный, но при этом явно видно, где тут синхронные части, а где асинхронные.
В Scala/Scala.js коде пример выше выглядел бы примерно следующим образом:
def handleRequestArrows(user: User, sender: ResultSender): Future[SendingResult] {
val res = for {
_ <- isUserValidAsync(user)
(data, rate) <- Future.sequence(Seq(getUserDataAsync(user), getRateAsync("service")))
savedData <- updateUserDataAsync(user, updateData(data, rate))
sendingResult <- sender.send(savedData)
} yield sendingResult
res.andThen { // обычно это делают на уровне выше, но я добавил сюда, чтобы не отступать от примера
case Failure(e) => logger.log("An error ocurred", e)
}
}
Код вполне наглядный. Видно, что за чем идет. Если какой-то шаг вернет ошибку, то выполнение всего блока for-comprehension прервется.
И чем стрелочка и for принципиально отличаются от await и async соответственно? :-)
Все асинхронные функции явно вынесены внутрь for блока. В случае с async/await синхронные и асинхронные функции перемешаны на одном уровне.
Отличие в том, что сам по себе синтаксис и конструкции языка не форсируют такой подход. Да, приведенный пример показывает, что можно явно разделить асинхронный и синхронный код и всё будет хорошо. Но это не значит что нельзя не разделять асинхронный и синхронный код. Получается страховка от ошибок на уровне договоренностей, а не на уровне конструкций языка. И именно в этом проблема.
Можно. Примерно так:
for {
resAsync1 <- someAsyncFun1(args)
resSync = someSyncFun(resAsync1)
resAsync2 <- someAsyncFun2(resSync)
} yield resAsync2
Но не страшно выполнять синхронный код в асинхронном блоке, это потенциально не приведет к ошибке. Страшнее выполнить асинхронный код в синхронном блоке и забыть await. Поэтому я и утверждаю, что такое разделение — это хорошо.
Если ты засунешь эти три строки в for-comprehension — то да, будет ошибка компиляции.
Насчет TypeScript — в нем нету аналога for-comprehension. То есть там будет такая же ситауция, как и в js.
someSyncFun
принимает не Promise
, то, если не за-await-ить someAsyncFun1
, ошибка будет. Но это практически бесполезная полумера. В принципе можно еще руками проставить тип resAsync1
, тогда тоже будет ошибка, но об этом тоже нужно помнить.Да, при несовпадении типов дальше по коду можно выловить часть ошибок. Но если функция с сайд эффектом и ничего не возвращает, а нам нужно дождаться этого сайд эффекта, то мы просто получим race condition, если забыли await. В моем примере выше на scala.js даже функция без возвращаемого значения (isUserValidAsync
) завернута в for-comprehension и явно видно, что она является частью асинхронного блока.
> И мы вдруг понимаем, почему код на основе async/await смотрелся странно.
Но ведь async/await — просто частный случай do-нотации (привет, haskell) в приложении к promise-монаде.
Но это все-таки не совсем она, хотя и очень похоже.
Для понимания разницы: в скала аналогом do notation является for-comprehension, но есть и библиотека Each, обобщающая async/await на все монады.
Естественно эти подходы эквивалентны настолько же, насколько они оба эквивалентны явным вызовам point и bind, но отличия при написании кода заметны.
Async/await это шаг назад для JavaScript'a?