Pull to refresh

Comments 115

Так и не понял аргументов автора. Судь async/await как раз в том, чтобы сделать асинхронный код максимально похожим на обычный, чтобы его было проще понимать и легче отлаживать.

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

const a = await getA();
const b = await getB(a);
const c = await getC(a, b);

Аналогичный код с promise'ами выглядит очень гадко.
У статьи, конечно, несколько громкий заголовок — async/await — вовсе не шаг назад, особенно если программировать в императивном стиле (а он именно для этого и появился — не все пишут в функциональном и/или реактивном стиле). Аргумент автора лишь в том, что когда привыкаешь к функциональному подходу, он сам по себе уже кажется чище и элегантнее.

У меня 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 функции будут поддерживаться без дополнительных проблем, то мой вариант конечно же не проще.

Если мне не изменяет память, конструкция asparallel/select — это rxjs. Круто. Притащили rx.
Вот мы и подхошли к мысли, что все подходы хороши, но при одном условии, если их использовать в нужных местах=)
А что плохого в том что бы использовать несколько подходов? Религия не позволяет?
Если вы сейчас про .Net, то странно его упоминать в контексте обсуждения «async/await или Promise в JS».
Если вы все же про JS, то зачем «несколько подходов» для стандартного способа распараллеливания и сбора результатов Promise.all? Уже даже await* задеприкейтили в пользу последнего.
По сути map который возвращет таски, которые инкапсулируют елементы массива и позволяют их обрабатывать парелельно. Я его так назвал по аналогии с миром .Net где async/await єсть уже относительно давно и метод AsParallel стал дополнением стандартной библиотеки. Имя может быть другим не в этом суть. Суть том что вместе с раширением возможностей языка будут расширены и возможности библиотек, не стоит об этом забывать.

На ваш код Babel ругается:
https://babeljs.io/repl/#?babili=false&evaluate=true&lineWrap=false&presets=es2015%2Ces2016%2Ces2017%2Cstage-3&code=async%20function%20fetchAll(urls)%20%7B%0A%20%20%20%20return%20urls.asParalell().select(url%20%3D%3E%20await%20fetch(url))%3B%0A%7D


И он прав, потому что async написан у корневой функции, а await внутри другой, которая идет аргументом в map. Async так не работает.

Так ведь async/await и не отменяет промисы. Там где нужна параллельность, действительно можно использовать Promise.all(), но в моем примере нужна была именно последовательность.
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, не вижу причин спорить

Вариант с атомами:


fetchAllParallel( urls ) {
    return urls.map( url => $mol_resource.item( url ).text() )
}
fetchAllSerial( urls ) {
    return urls.map( url => $mol_resource.item( url ).text().valueOf() )
}

Вроде же изначально договорились никакие библиотеки не использовать.


Так-то я могу тоже взять гипотетическую библиотеку 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. Тестировать такую функцию почти невозможно =/

Вы серьезно считаете, что это читабельней?
Вы ведь знаете что я хочу сказать, да?
Плевать на нечитаемость! Зато другие хипстеры на афтерпати будут восхищены!

Да успокойся ты, глупый хипстерофоб. Я же приписал в конце — что функция убогая и не тестируемая сама по себе, так что я ошибся когда вообще попытался написать ее "красиво"

Я глупый? Ну-ну. Прекрасная попытка оскорбления! Сразу показывает ваш уровень и силу аргументов. Теперь то очевидно, что вы были правы и промисы значительно лучше async-await, спасибо, что открыли мне глаза.

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

Давайте так, я пытался и у меня не получилось, ок?

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) мне нравится куда больше.
Это к чему? Тут как раз, скорее всего, async-await не нужен, потому что, скорее всего, нет смысла обрабатывать URL'ы по очереди, было бы лучше «одновременно».
fucntion fetchAll(urls) {
  return Promise.all(urls.map(fetchAsync));
}
Спасибо, я знаю про то что их можно запустить параллельно.
Но смысл есть, когда количество урлов идет на тысячи.
Когда на тысячи я использую хак (а может так и надо?) в виде пула, но я полностью за Ваш вариант с async-await по сравнению с примером выше с reduce, если нужно именно последовательно.

Ограничивать число одновременных запросов можно на уровне модуля http.
Зачем засорять логику приложения такими подробностями?

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

Ну, я написал "можно одновременно посылать несколько запросов"


А у вас последовательные. Это не одновременно.

Так и не понял аргументов автора.

"Async/await плохо, потому что это императивно, а значит не модно, а нас ведь считают крутыми, потому что мы пишем на ФП."


Без шуток. Это аргументация профессиональных программистов. И они правда так думают.

У нас, так исторически сложилось, что тесты написаны асинхронно. Правда на Scala, но сути это не меняет.
При этом используются целых три стиля — flatMap (на сколько я понимаю, это в js называется «через callback»), с помощью for (в js аналога, по моему, нет, похоже на do в Haskell), и async/await.
Для меня с flatMap единственное неудобство — большой уровень вложенности синтаксических конструкций (соответственно проблема, как поступать с отступами). for выглядит чище, я последнее время склоняюсь к нему. Стиль async/await имеет много фанатов, но мне от показался малопонятным (я давно программирую на функциональных языках и более императивный код мне читать сложнее). Кроме того, в этом коде очень легко где-нибудь забыть написать await на какую-нибудь фьючу, результат которой игнорируется (в нашем случае это обычно было удаление созданных для теста сущностей), и получить race condition.
В общем, async/await мне не понравился.
  1. промисы не решали в понятном виде большое количество проблем. Например — использование while или switch-case внутри логики (а особенно все вместе).
  2. правильно воспринимать их не как сахар, а как специфическую инструкцию, которая делает 2 отдельных вещи: уступает поток исполнения и ожидает выполнения результата. Мы можем использовать ее и для того, и для другого.
  3. async/await действительно является шагом назад под давлением внешних факторов, но не потому что она не функциональная, а потому что реализует подмножество функционала функций-генераторов, и не имеет особого смысла (кроме возможностей для подсветки и простоты самой по себе). Я бы предпочел видеть синтаксис вроде Promise.generator(function() {}), он бы дал куда больше гибкости и возможностей для тестирования — как минимум за счет инструкции yield.
  4. то, что вы любите функциональное программирование, не значит, что его любят все. JS пытается стать универсальным языком, а не вторым OCaml.
UFO just landed and posted this here

А смысл? сделали же уже async/await. Много разных API это не хорошо.
А асинки уже вошли в es2017, если не ошибаюсь.

Изучаем новые слова, такие как функторы, монады, моноиды и вдруг наши 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, библиотеки для дат, да чего там только не было, и так педалили, что дальше носа ничего не видели. Ну как? :)
Вот и тут, хорошим программист станет тогда, когда сам наберется опыта, лично наступив на каждую граблю. Я понимаю вашу усталось от всего этого фанбойства, но с этим ничего не поделать.
сейчас очень трудно найти себе в команду хороших программистов.
Горькая правда
Ну на самом деле вы зря недооцениваете влияние подобных статей и комментариев на начинающих программистов. Оно огромно.

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

Лично я очень много брал именно из комментариев. Статья на какую-либо тему, а в комментариях в пределах аргументированного спора ты видишь две противоположные мысли. Они между собой спорят, не хотят уступить, не слышат аргументов друг-друга, но ты то когда новичок и читаешь это — не имеет реально сформированного мнения, потому слышишь аргументы одной и другой стороны. С какими-то не согласен, какие-то вообще не понимаешь, т.к. не имеешь опыта, какие-то ошибочные. Но это — яркий способ понять вещи, которые в книжке описано намного хуже.

Вот спорит сторонник фреймворка А и фреймворка Б, каждый говорит, что его лучше, а другой хуже. А ты читаешь и узнаешь, вот, оказывается, если смотреть на фреймворк А под таким углом, то он и ничего вообще-то, а вот эта фича в фреймворке Б оказывается так используется.
Судя по оповещению, вы еще спрашивали:
А что смешного-то?

Да много чего.
— Смешно то, что «императивный подход — это девяностые, а функциональный — это десятые».
— А еще, что это считается важным аргументом в технической статье.
— Что думают, что пишут на JS в функциональном стиле, а в коде не имеют ни чистоты ни декларативности
— Что используют алгоритм квадратичной сложности, где более читаемый и очевидный алгоритм линейной сложности просто чтобы код казался функциональным.
— Потому что мерилом качества кода выступает не «мы поддерживаем огромный продукт уже 3 года, но все-равно с легкостью и минимум багов вносим в него функционал», а «наши dev-друзья начинают считать нас крутыми, потому что мы говорим эти странные слова довольно часто!»
— Смешна не функциональная парадигма, она очень крута в определенных областях, смешны люди, которые ее продвигают подобными статьями.
Да я что-то задумался, и подумал, что вы это про термины теории категорий. Ну и удалил, когда перечитал внимательней.
Считаю, все эти парадигмы довольно условными и личными для каждого. Отдавать преимущество только async/await или только Promise, только потому что оно не удовлетворяет какой то парадигме на мой взгляд довольно странно. Не понимаю, почему async/await шаг назад.

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

Программисты 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".

UFO just landed and posted this here

Сахар сахару рознь… Хороший сахар делает явное ещё более явным, плохой — явное делает неявным.

Generalized async return types в C# я уже пощупал — фигня это, правда, тем более, что они все равно на Task завязаны. Прироста в производительности по сравнению с обычным Task вообще нет — основной оверхед там от state machine идёт. Теоретически, польза от этого есть при полном переписывании штатного TPL на свой, с блекджеком и шлюхами.

Так я и не имел в виду, что будут какие-то действительно полезные или масштабные доработки. Это скорее затыкание дыр в абстракции "асинхронный код, похожий на синхронный", которая дырява by design.

Наоборот. Асинхронный код — костыль для многозадачности в рамках одного потока. В нормальных языках есть потоки и/или сопрограммы. И те и другие — синхронны.

Так и есть. В чём принципиальная разница между сотней асинхронных задач и сотней потоков, ожидающих выполнения? Да ни в чём. Просто механизмы синхронизации на уровне операционной системы дорогие, вот и приходится выкручиваться с помощью кооперативной многозадачности в юзерспейсе.

Просто механизмы синхронизации на уровне операционной системы дорогие, вот и приходится выкручиваться с помощью кооперативной многозадачности в юзерспейсе.
Речь не про уровень ОС, а про корутины на уровне программы которая написана синхронным кодом. Дак вот по сравнению с ним, асинхронный код — костыль.

В том, что потоков будет не сотня, а на порядок меньше. Сопрограмм (которые могут жить и на одном потоке) тоже будет на порядок меньше. И только асинхронное программирование требует разбиения бизнес задачи на множество мелких асинхронно исполняющихся технических подзадач вида "обработать ответ от такого-то апи и вызвать следующее".

В этом случае попытки замаскировать асинхронный код под синхронный проблему не решают. Если асинхронный код — это костыль, то async/await это костыль для другого костыля. Настоящий kostyl-driven design во всей своей красе.

Конечно костыль, поэтому мы с помощью реактивности абстрагируемся от (а)синхронности.

Майкрософт вообще частенько грешит подобным подходом. Ту же самую ошибку они допустили, когда выкатывали Web Forms, в котором пытались сделать разработку web приложений похожей на Windows Forms. Естественно «не взлетело». Вместо этого «взлетели» те походы, которые не скрывали особенности web разработки, а наоборот подчёркивали их и учитывали в своей архитектуре. За счёт этого им удалось сделать web разработку более простой и удобной.

O, да! Web Forms — это был гораздо более крутой изврат… Я помню фееричный анонс UpdatePanel (для тех, кто не в теме, это такой компонент на форму внутри которого автоматически AJAX типа работает) от MS-евангелиста, вот это был реальный треш и угар.

Асинки не параллельные. Код все равно последовательный, просто код не виснет когда ожидает ответа.

Я совсем не эксперт в функциональном программировании и в async-await в JS, но, на мой взгляд, пример в статье совсем не отражает её суть.
В частности, практически каждый метод имеет побочные эффекты, любой метод может изменить внутреннее состояние аргумента, и, в общем-то, часто именно эти явления и ожидаются. То есть, идеологически полный провал.
Кстати, `isUserValidAsync` было бы лучше назвать assertValidUserAsync или использовать по-другому.

С другой стороны, в случае использования async-await, на мой взгляд, существенно улучшается читабельность кода от чего напрямую зависит его качество и стоимость поддержки. Хотелось бы заметить, что читабельность улучшается за счет меньшего количества всяких скобочек, запятых, вызываемых методов и вообще меньше кода.

При этом никто не запрещает использовать элементы функционального подхода, только без лишних ограничений на способ выражения желаемого. К тому же, наверняка, JS-движок следующей версии сможет существенно оптимальнее представить внутри себя async-await конструкции, чем свалку promise'ов, а программист все так же может продолжать использовать более менее простую модель с выворачиванием этого всего в promise'ы.
К сожалению ничего не могу сказать по поводу async/await, но вот при использовании Promise в продакшене столкнулись с такой проблемой, что стали появляться синхронно-асинхронные функции:
function doSomeWork(request) {
    // код...
    // где в середине
    if (request == null) {
        return;
    }
    // где-то в конце, doPost возвращает Promise.
    return request.doPost(/**...*/);
}

из-за чего лезут нехорошие баги. Хотя это не проблема Promise, а их некорректное использование.

На счет статьи показалось, что автор поведал нам хронологическую ленту развития JS
Попахивающий код. Сигнатура функции вообще произвольная (тип возвращаемого не определен)? Что мешает всегда возвращать промис (Promise.reject() например)?
Код вполне типичный. Просто во всех местах где будет юзаться эта функция делается что-то типа такого:
const result = doSomeWork(request);
if (!result) return;
// Дальше нормальное выполнение
Да такие ветки имеются, и даже такие Promise.resolve(doSomeWork(request))
Как раз в таком примере as/aw вас спасёт: любая async-функция всегда вернёт промис
Promise.resolve вы наверное имели в виду? Да ничего и про сигнатуру Вы правы, я просто делюсь что имеем по факту.
Решение выглядело бы примерно так:

Не так, вы забыли позаворачивать все хендлеры в try-catch-и, иначе любая ошибка убьёт процесс.

Их не надо заворачивать в try-catch, потому что верхний try-catch поймает все исключения ниже

Речь о коде на колбяках. Один try-catch ничего не поймает.

Ну на самом деле async/await скорее можно назвать синтаксическим сахаром для CPS, так что обвинения в императивности, как минимум, выглядят голословными. Как инструмент async/await довольно удобен, проблема опять в том, как его использовать.
Я сейчас скажу довольно мрачную и пессимистическую вещь, отрицающую плоды прогресса даже сильнее, чем вышепереведённая мысль отрицает их.

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

Дело в том, что у промисов есть метод «Promise.all», тогда как у библиотеки Async есть не только ананалогичный ему метод «async.parallel» (а также другой метод «async.waterfall», аналогичный цепному вызову промисов через их «.then»), но и ещё почти шестьдесят других удобных методов. Оговорюсь, что из них лично я использую никак не больше десятка, но, во-первых, даже этот десяток не горю желанием сочинять самостоятельно ради перехода на промисы, а во-вторых, меня радует сознание того, что за пределы этого десятка я всегда смогу (при малейшей необходимости) выйти почти мгновенно, потому что там ждут меня ещё полсотни готовых методов, которые опять же не придётся сочинять самостоятельно.

То есть переход на промисы выглядит как переход на новый (и ещё не обросший возможностями) фреймворк, ради которого отбрасывается история многих лет эволюции библиотеки Async (начиная от первого коммита 13 мая 2010 года) и большинство обретённых ею возможностей, так что теперь промисовая публика будет переизобретать велосипеды (причём, скорее всего, опять же на протяжении нескольких лет кряду) при столкновении с наималейшими проблемами действительности.

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

Поэтому лучшее, что можно сделать с промисом — это сунуть его в «async.asyncify» и дальше коллбекнуть по старинке.

Your callback hell is my home.
Всё так и есть, не поспорить
Но async-await всё равно удобные. Удачно удалось сократить один модуль на промисах, переписав его на async-await, на 40%, и добавлять в него новый функционал стало проще и приятнее ^__^
По этой логике можно вообще не развивать язык и пусть всю работу делают фреймворки. Зачем вам перебирающие методы массива (map, filter и т.д.), если у вас есть цикл for? Если нужно что-то сложнее, то используйте фреймворки, типа lodash. Собственно, именно из-за такой логики мы и находимся в ситуации, когда до 2015 года (больше чем за 15 лет своего существования), JS-стандарт обновлялся только два раза. Именно поэтому JS стал восприниматься как ассемблер на котором невозможно что-то сложное написать, а шутка про день, когда не было создано ни одного нового JS-фрейморка уже набила оскомину.
То что «фреймворк» делают частью языка меняет абсолютно все. Как минимум, можно быть уверенным что выполнение подобных задач будет оптимизировано на уровне интерпретатора и это положительно скажется на производительности.
По моему всё гораздо проще: Удобно использовать async/await => используем async/await. Удобно Promise писать, пишем Promise. Если код дорого переписывать на FP, оставляем императив. Это более чем Advanced javascript. Новички всё равно будут отстреливать себе ноги и с тем и с другим и с третьим. Если ты профи с FP, это не означает что ты никогда не будешь писать/обновлять OOP. Если ты катаешься по конференциям и твой твиттер полон фэнов хипстеров, это не означает что ты всегда прав. В конце концов даже Дуглас Крокфорд был неправ по поводу;

PS: и для Promise и для async/await пока без полифилов можно писать только на Node.js и прототипы в хроме канари. Сафари не поддерживает без полифилов fetch API, Edge… нет на Маках, Хрома нет на IOS Но господи как я рад что сейчас не 2009 год и это самые большие проблемы хайпа.
Babel 6, Webpack 2, React 15, ES6+ сделали жизнь на много проще.
UFO just landed and posted this here

В 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 соответственно? :-)

UFO just landed and posted this here

Что такое стрелочка как не await?

UFO just landed and posted this here

Все асинхронные функции явно вынесены внутрь for блока. В случае с async/await синхронные и асинхронные функции перемешаны на одном уровне.

UFO just landed and posted this here

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

UFO just landed and posted this here

Можно. Примерно так:


for {
    resAsync1 <- someAsyncFun1(args)
    resSync = someSyncFun(resAsync1)
    resAsync2 <- someAsyncFun2(resSync)
} yield resAsync2

Но не страшно выполнять синхронный код в асинхронном блоке, это потенциально не приведет к ошибке. Страшнее выполнить асинхронный код в синхронном блоке и забыть await. Поэтому я и утверждаю, что такое разделение — это хорошо.

UFO just landed and posted this here

Если ты засунешь эти три строки в for-comprehension — то да, будет ошибка компиляции.
Насчет TypeScript — в нем нету аналога for-comprehension. То есть там будет такая же ситауция, как и в js.

Если someSyncFun принимает не Promise, то, если не за-await-ить someAsyncFun1, ошибка будет. Но это практически бесполезная полумера. В принципе можно еще руками проставить тип resAsync1, тогда тоже будет ошибка, но об этом тоже нужно помнить.

Да, при несовпадении типов дальше по коду можно выловить часть ошибок. Но если функция с сайд эффектом и ничего не возвращает, а нам нужно дождаться этого сайд эффекта, то мы просто получим race condition, если забыли await. В моем примере выше на scala.js даже функция без возвращаемого значения (isUserValidAsync) завернута в for-comprehension и явно видно, что она является частью асинхронного блока.

Понятное дело, я и не спорю, мне такой формат записи по душе. Увы, ни JS, ни TS не предоставляют таких строгих конструкций.
UFO just landed and posted this here

Не скомпилируется. for-comprehension всегда требует что-то возвращать в левую часть стрелки. Если возвращать нечего — то используется запись _ <- blabla

> Мы уже использовали promise-монаду в прошлом со всеми сопутствующими преимуществами, но в то время мы просто не знали это слово!
> И мы вдруг понимаем, почему код на основе async/await смотрелся странно.

Но ведь async/await — просто частный случай do-нотации (привет, haskell) в приложении к promise-монаде.
Думаю в JS еще придут к do notation.
Но это все-таки не совсем она, хотя и очень похоже.
Для понимания разницы: в скала аналогом do notation является for-comprehension, но есть и библиотека Each, обобщающая async/await на все монады.
Естественно эти подходы эквивалентны настолько же, насколько они оба эквивалентны явным вызовам point и bind, но отличия при написании кода заметны.
Sign up to leave a comment.

Articles