Как разравнять Пирамиду смерти

    Настроить webpack по мануалу, запрограммировать ангуляр и даже послать json по ajax — кажись каждый может, но вот как взглянешь на сам код… В этом посте будет показана разница между нововведениями.

    Итак вы открыли ноду и увидели, что почти все функции «из коробки» последним аргументом принимают колбэк.

    var fs = require("fs");
    fs.readdir(__dirname, function(error, files) {
        if (error) {
            console.error(error);
        } else {
            for (var i = 0, j = files.length; i < j; i++) {
                console.log(files[i]);
            }
        }
    });
    


    Пирамида смерти


    А в чем собственно проблема? Проблема в том, что на маке с ретиной порой заканчивается место под пробелы (конечно можно сказать, что 4 пробела на таб — роскошь) и весь код маячит далеко справа при использовании хотя бы десятка таких функций подряд.



    var fs = require("fs");
    var path = require("path");
    var buffers = [];
    
    fs.readdir(__dirname, function(error1, files) {
        if (error1) {
            console.error(error1);
        } else {
            for (var i = 0, j = files.length; i < j; i++) {
                var file = path.join(__dirname, files[i]);
                fs.stat(file, function(error2, stats) {
                    if (error2) {
                        console.error(error2);
                    } else if (stats.isFile()) {
                        fs.readFile(file, function(error3, buffer) {
                            if (error3) {
                                console.error(error3);
                            } else {
                                buffers.push(buffer);
                            }
                        });
                    }
                });
            }
        }
    });
    
    console.log(buffers);
    


    Так что же c этим можно сделать? Не применяя библиотек, для наглядности, так как с ними все примеры не займут и строчки кода, дальше будет показано как с этим справиться используя сахар es6 и es7.

    Promise

    Встроенный объект позволяющий немного разравнять пирамиду:

    var fs = require("fs");
    var path = require("path");
    
    function promisify(func, args) {
        return new Promise(function(resolve, reject) {
            func.apply(null, [].concat(args, function(error, result) {
                if (error) {
                    reject(error);
                } else {
                    resolve(result);
                }
            }));
        });
    }
    
    promisify(fs.readdir, [__dirname])
        .then(function(items) {
            return Promise.all(items.map(function(item) {
                var file = path.join(__dirname, item);
                return promisify(fs.stat, [file])
                    .then(function(stat) {
                        if (stat.isFile()) {
                            return promisify(fs.readFile, [file]);
                        } else {
                            throw new Error("Not a file!");
                        }
                    })
                    .catch(function(error) {
                        console.error(error);
                    });
            }));
        })
        .then(function(buffers) {
            return buffers.filter(function(buffer) {
                return buffer;
            });
        })
        .then(function(buffers) {
            console.log(buffers);
        })
        .catch(function(error) {
            console.error(error);
        });
    


    Кода стало немного больше, но зато сильно сократилась обработка ошибок.

    Обратите внимание .catch был использован два раза потому, что Promise.all использует fail-fast стратегию и бросает ошибку, если ее бросил хотя бы один промис на практике такое пременение далеко не всегда оправдано, например если нужно проверить список проксей, то нужно проверить все, а не обламываться на первой «дохлой». Этот вопрос решают библиотеки Q и Bluebird и тд, поэтому его освещать не будем.

    Теперь перепишем это все с учетом arrow functions, desctructive assignment и modules.

    import fs from "fs";
    import path from "path";
    
    function promisify(func, args) {
        return new Promise((resolve, reject) => {
            func.apply(null, [...args, (err, result) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(result);
                }
            }]);
        });
    }
    
    promisify(fs.readdir, [__dirname])
        .then(items => Promise.all(items.map(item => {
            const file = path.join(__dirname, item);
            return promisify(fs.stat, [file])
                .then(stat => {
                    if (stat.isFile()) {
                        return promisify(fs.readFile, [file]);
                    } else {
                        throw new Error("Not a file!");
                    }
                })
                .catch(console.error);
        })))
        .then(buffers => buffers.filter(e => e))
        .then(console.log)
        .catch(console.error);
    
    


    Generator

    Теперь совсем хорошо, но…ведь есть еще какие-то генераторы, которые добавляют новый тип функций function* и ключевое слово yeild, что будет если использовать их?

    import fs from "fs";
    import path from "path";
    
    function promisify(func, args) {
        return new Promise((resolve, reject) => {
            func.apply(null, [...args, (err, result) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(result);
                }
            }]);
        });
    }
    
    function getItems() {
        return promisify(fs.readdir, [__dirname]);
    }
    
    function checkItems(items) {
        return Promise.all(items.map(file => promisify(fs.stat, [path.join(__dirname, file)])
            .then(stat => {
                if (stat.isFile()) {
                    return file;
                } else {
                    throw new Error("Not a file!");
                }
            })
            .catch(console.error)))
            .then(files => {
                return files.filter(file => file);
            });
    }
    
    function readFiles(files) {
        return Promise.all(files.map(file => {
            return promisify(fs.readFile, [file]);
        }));
    }
    
    function * main() {
        return yield readFiles(yield checkItems(yield getItems()));
    }
    
    const generator = main();
    
    generator.next().value.then(items => {
        return generator.next(items).value.then(files => {
            return generator.next(files).value.then(buffers => {
                console.log(buffers);
            });
        });
    });
    


    Цепочки из generator.next().value.then не лучше чем колбэки из первого примера однако это не значит, что генераторы плохие, они просто слабо подходят под эту задачу.

    Async/Await

    Еще два ключевых слова, с мутным значением, которые можно попробовать прилепить к решению, уже надоевшей задачи по чтению файлов- Async/Await
    import fs from "fs";
    import path from "path";
    
    function promisify(func, args) {
        return new Promise((resolve, reject) => {
            func.apply(null, [...args, (error, result) => {
                if (error) {
                    reject(error);
                } else {
                    resolve(result);
                }
            }]);
        });
    }
    
    function getItems() {
        return promisify(fs.readdir, [__dirname]);
    }
    
    function checkItems(items) {
        return Promise.all(items.map(file => promisify(fs.stat, [path.join(__dirname, file)])
            .then(stat => {
                if (stat.isFile()) {
                    return file;
                } else {
                    throw new Error("Not a file!");
                }
            })
            .catch(console.error)))
            .then(files => {
                return files.filter(file => file);
            });
    }
    
    function readFiles(files) {
        return Promise.all(files.map(file => {
            return promisify(fs.readFile, [file]);
        }));
    }
    
    async function main() {
        return await readFiles(await checkItems(await getItems()));
    }
    
    main()
        .then(console.log)
        .catch(console.error);
    


    Пожалуй самый красивый пример, все функции заняты своим делом и нету никаких пирамид.

    Если писать этот код не для примера, то получилось бы как-то так:

    import bluebird from "bluebird";
    import fs from "fs";
    import path from "path";
    
    const myFs = bluebird.promisifyAll(fs);
    
    function getItems(dirname) {
        return myFs.readdirAsync(dirname)
            .then(items => items.map(item => path.join(dirname, item)));
    }
    
    function getFulfilledValues(results) {
        return results
            .filter(result => result.isFulfilled())
            .map(result => result.value());
    }
    
    function checkItems(items) {
        return bluebird.settle(items.map(item => myFs.statAsync(item)
            .then(stat => {
                if (stat.isFile()) {
                    return [item];
                } else if (stat.isDirectory()) {
                    return getItems(item);
                }
            })))
            .then(getFulfilledValues)
            .then(result => [].concat(...result));
    }
    
    function readFiles(files) {
        return bluebird.settle(files.map(file => myFs.readFileAsync(file)))
            .then(getFulfilledValues);
    }
    
    async function main(dirname) {
        return await readFiles(await checkItems(await getItems(dirname)));
    }
    
    main(__dirname)
        .then(console.log)
        .catch(console.error);
    
    ua-hosting.company
    Хостинг-провайдер: серверы в NL / US до 100 Гбит/с

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

      0

      У меня велосипед получился точно такой же. С точностью до названий функций. А переименую-ка я их как у Вас...

        –13
        Кто нибудь скажет мне чем promise лучше «классического ajax»?
          +15
          Тем же, чем мягкое лучше теплого.

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

          Справедливости ради, если не лепить анонимные функции, а грамотно все декомпозировать и разносить по модулям, то и без промисов все выглялит вполне прилично. Но промизы действительно легко компонуются.
          +6
          это не из разряда лучше, а скорее вместе
            0
            github.com/yortus/asyncawait — замечательная либа
              +2

              Последняя версия кода с отдельными функциями сморится приятно, но как по мне изначальная версия гораздо понятнее.
              А почему нельзя, например


                  if (error) {
                      console.error(error);
                  } else {
                      for (var i = 0, j = files.length; i < j; i++) {
                          console.log(files[i]);
                      }
                  }

              Заменить на


                  if (error) {
                      console.error(error);
                      return;
                  }
                  for (var i = 0, j = files.length; i < j; i++) {
                       console.log(files[i]);
                  }

              ?

                +1
                Предполагаю, потому, что этот код в цикле и выход из функции там не нужен. Всё-таки обрабатывается список файлов.
                  +1

                  В примере все условия внутри лямбд.


                  fs.readdir(__dirname, function(error1, files) {
                      if (error1) {
                          console.error(error1);
                          return;
                      }
                  
                      var fs_stat = function(error2, stats) {
                          if (error2) {
                              console.error(error2);
                              return;
                          }
                          if (!stats.isFile()) {
                              return;
                          }
                          fs.readFile(file, function(error3, buffer) {
                              if (error3) {
                                  console.error(error3);
                                  return;
                              }
                              buffers.push(buffer);
                          });
                      };
                  
                      for (var i = 0, j = files.length; i < j; i++) {
                          var file = path.join(__dirname, files[i]);
                          fs.stat(file, fs_stat);
                      }
                  });

                  По-моему вполне читаемо, и не размашисто.

                +4
                Проблема в том, что на маке с ретиной порой заканчивается место под пробелы

                Приятно, что индустрия после первого шага «Оптимизировать дорого, докупим серверов» ещё не сделало второго «Если код содержит божественные объекты и избыточную вложенность, просто купить монитор с диагональю побольше»
                  +1
                  Без промисов использую такой подход:

                  $.post('/url', {data: 'data'}, onResponseCallback1);
                  
                  function onResponseCallback1() {
                      
                      $.post('/url', {data: 'data'}, onResponseCallback2);
                  
                  }
                  
                  function onResponseCallback2() {
                      
                      //...
                  
                  }
                  


                  На ноде с промисами:

                  Promise.resolve()
                      .then(onResponseCallback1)
                      .then(onResponseCallback2)
                      .catch(onError)
                  
                  function onResponseCallback1(data) {
                      
                      return data;
                  
                  }
                  
                  function onResponseCallback2(data) {
                      
                      return data;
                  
                  }
                  
                  // Для обработки ошибок
                  function onError() {}
                  


                  Никаких пирамид, функции можно делать чистыми, на мой взгляд более нагляден порядок выполнения. Возможно известная практика, на открытие Америки не претендую.
                    0
                    Какие же они чистые, если в них вызывается асинхронный код и они при этом сами ничего не возвращают
                      –1
                      А я и не говорил, что они чистые.
                    –7
                    Переходите на php, и забудьте про эти костыли
                      +3
                      … и вспомните про новые, более костыльные костыли, нежели эти ваши асинхронные костыли.
                      0
                      Первый пример (который без промисов) точно будет работать так, как задумано? Ведь fs.readdir асинхронна, так что помешает console.log выполниться сразу после неё, до того, как управление попадёт в коллбэк? Тогда в логе окажется пустой массив.
                      Ну и помимо указанных способов можно делать так, как обычно делают все новички в JS, впервые столкнувшиеся с асинхронной лапшой — т.е. поименовать все коллбэки, расположив их друг за другом (выше уже сказали о такой возможности):
                      fs.readdir(__dirname, processFiles );
                      
                      function processFiles(error1, files) {
                          if (error1) {
                              console.error(error1);
                          } else { 
                              beginReading(files); 
                          };
                      }
                      
                      function beginReading(files) {
                          for (var i = 0, j = files.length; i < j; i++) {
                              var file = path.join(__dirname, files[i]);
                              fs.stat(file, getFileStats);
                          }
                      }
                      
                      function getFileStats(error2, stats) {
                          if (error2) {
                              console.error(error2);
                          } else if (stats.isFile()) {
                              fs.readFile(file, fileReaded);
                          }
                      };
                      
                      function fileReaded(error3, buffer) {
                          if (error3) {
                              console.error(error3);
                          } else {
                              buffers.push(buffer);
                          }
                      }
                      

                      Но вообще лапша в коде в JS такая же неизбежность, как в C++ утечки памяти. Вроде и инструментов куча, чтоб этого избежать, и подходов множество, а всё равно на мало-мальски большом проекте оно проявляется.
                        0
                        да `buffers` будет пустой, он там для того что бы показать куда попали данные, иначе непонятно что делает код, так как это даже не функция

                        поименовать колбеки, кстати полезно не только новичкам, но и для отладки, особенно если код был пропущен через babel
                        –1
                        Но вообще лапша в коде в JS такая же неизбежность, как в C++ утечки памяти.

                        Ну не знаю — ни в js ни в typescript не наблюдаю ни лапши ни пирамид. А еще в копилку перечисленного — @autobind декоратор, помогает при использовании методов класса в качестве коллбэков.

                          –1
                          объясните чем хорошо @autobind. то есть я понимаю его смысл и зачем он нужен
                          однако в текущей реализации он ущербен https://github.com/jayphelps/core-decorators.js/issues/76
                          а для того что бы использовать его в лоб достаточно двойного двоеточия `::obj.method` (вот же ж этот камент про пхп сверху)
                            –1

                            Вы:


                            однако в текущей реализации он ущербен
                            @autobind doesn't work properly when calling super from the parent's parent's parent.
                            @autobind
                            class A {
                            method() {
                            console.log(this.test);
                            }
                            }

                            Я:
                            @autobind декоратор, помогает при использовании методов класса в качестве коллбэков.


                            Так то никто Вам не мешает сальто в прыжке с переворотом делать, просто я то не об этом говорил.

                              –1
                              а я как раз об этом — для использования «в лоб»

                              promise.then(::obj.method)
                              


                              достаточно встроенного сахара и не нужно подключать кучу дополнительных либ
                                0

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

                          0
                          Но вообще лапша в коде в JS такая же неизбежность, как в C++ утечки памяти.
                          Ну, не знаю — у меня в таком знаете ли нефиговом проекте на С++ (24*7) утечек памяти не наблюдается

                          А вообще, это же обычная прогулка через «тернии к звездам» когда успешный сценарий один, а возможных ошибок мильон:
                          bool bOk = false;
                          do{
                          if(что-то не так) break;
                          if(что-то не так) break;
                          if(что-то не так) break;
                          if(что-то не так) break;
                          if(что-то не так) break;
                          if(что-то не так) break;
                          if(что-то не так) break;
                          if(что-то не так) break;
                          bOk = true;
                          }while(0);
                          if(bOk) все получилось;
                          else обрабатываем ошибку;

                          Коды ошибок и выводы в лог — добавляются по вкусу.
                          В JS так нельзя?
                            +1
                            Можно, конечно! Если это синхронные операции.

                            Поскольку многопоточности в JS нету, а вешать весь браузер на каждый ajax-запрос никому не хочется, выкручиваемся асинхронностью — запрашиваем операцию и передаем ссылку на функцию, которую надо вызвать по ее окончании. И вот вроде бы и параллельность есть и программист себе последнюю ногу не отстрелил (ох уж эти программисты, на что только не идут, лишь бы не чинить баг в race condition).
                            Вот только если наспех код херачить, то вот такая «пирамидка» получается.
                                +1
                                Да я и не спорю. Просто объяснил товарищу, как мы тут в JS живем.
                                  +2
                                  Спасибо, друзья! Реально — интересно. Ощущение, что «на чужой раён» забрел, но любопытно же )
                                  Поскольку многопоточности в JS нету, а вешать весь браузер на каждый ajax-запрос никому не хочется, выкручиваемся асинхронностью — запрашиваем операцию и передаем ссылку на функцию, которую надо вызвать по ее окончании.
                                  Это понятно (на LUA похоже, там сплошь и рядом), но где, я извиняюсь, в Вашей ассинхронности таймауты? Про точки синхронизации даже не спрашиваю пока. Неужели сам браузер их определяет?
                                    +2

                                    Да, их определяет среда выполнения. Если вы сделаете setTimeout(myFunc, 1000), нет никакой гарантии, что myFunc выполнится через тысячу миллисекунд. Если в этот момент браузер (или nodejs) будет занят чем-то другим (например, обработкой какого-то события от пользователя), то «пусть весь мир подождет». Есть некоторый стэк вызовов, если он не пуст, то наш коллбэк вызовется, только когда он очистится.

                              0

                              Это называется resumable function. В С++ по этой же причине ввели future (=js promise) и вводят async/await.

                              –2

                              Информация для медитации: https://github.com/nin-jin/async-js/
                              Пулреквесты с реализацией на генераторах и асинкавайте приветствуются :-)

                                0

                                Кстати, первый код в посте и в комментариях — препаршивый, так как размазывает обработку ошибок по всему коду, вместо того, чтобы пробрасывать ошибку наверх и обрабатывать ошибки в одном месте. Вариант с промисами этого бага лишён.


                                Правильная обработка ошибок в ноде выглядит так:


                                var myAsyncFunction = ( ...args , done ) => {
                                    otherAsyncFunction( ( error, result1 ) => {
                                        if( error ) return done( error )
                                
                                        // do something with result1 and generate result2
                                
                                        done( null , result2 )
                                    } )
                                }

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

                                  0

                                  Кстати, я там запилил и на генераторах и на асинкавайте реализации. Можете сравнить.

                                  0
                                  Вот спасибо. Как раз сейчас с колбэками в JS разбираюсь.
                                    0
                                    для генераторов заюзать что-то типа CO и будет тоже самое что с и async/await… это к слову про «это самый красивый подход»… А разве async/await поддерживается в NodeJS?
                                      0
                                      то есть пирамиду из `generator.next().value.then` замели под ковер под названием СО и сказали, что это тру :)
                                        0
                                        Ну если вы используете генераторы, то вы явно будете использовать «СО»… это можно рассматривать как подключение любого стороннего модуля, так что да — это нормально, ИМХО. А как вы рассуждаете можно и кишки async/await наружу вытащить =)
                                          +1
                                          согласен если статья приводит пример с использованием `bluebird` то пример с `CO` ничем не хуже.

                                          а вот кишки async/await написаны на С++ так что сравнивать их можно только с чем-то подобным, например с https://github.com/SyntheticSemantics/ems
                                        0
                                        Используя транспайлеры(например babel) вполне поддерживается, причем практически в любом окружении.
                                        Более того, с помощью babel-а, можно использовать и кучу других синтаксических плюшек из будущих стандартов и даже то, что не планируется к стандартизации в языке(например react, flow)
                                        +1
                                        не могу смотреть как юзается переменная «file» в первой версии кода, — какое по вашему у неё будет значение во время выполнения fs.readFile(file, ...)? остальные версии вообще не понял, должно быть старею ))
                                        не пойму почему так не написать:
                                        function isNotError(err) {
                                            if(!err) 
                                                return true;
                                            console.error(err);
                                            return false;
                                        }
                                        function onReadDir(err, files) {
                                            if(isNotError(err)) {
                                                for (var i = 0; i < files.length; i++)
                                                    readFile(path.join(__dirname, files[i]));
                                            }
                                        }
                                        function readFile(file) {
                                            function onStat(err, stats) {
                                                if(isNotError(err) && stats.isFile()) 
                                                    fs.readFile(file, onRead);
                                            }
                                            function onRead(err, buffer) {
                                                if(isNotError(err))
                                                    buffers.push(buffer);
                                            }
                                            fs.stat(file, onStat);
                                        }
                                        fs.readdir(__dirname, onReadDir);
                                        
                                          0
                                          это вы не можете смотреть?
                                          после вот этого?
                                          function isNotError(err) {
                                              if(!err) 
                                                  return true;
                                              console.error(err);
                                              return false;
                                          }
                                          

                                            0
                                            вы так говорите, будто работающий код хуже неработающего. что-то я не понял к чему ваше замечание.
                                            но признаю, с «не могу смотреть» я, пожалуй, перегнул, — напротив, мне просто бальзам на душу, когда кто-то начинает устраивать замыкания внутри цикла, за что получает трудно уловимый баг, — тогда я смотрю на это высунув язык от радости и приговариваю: «я же говорил, что не надо использовать анонимные функции где попало».
                                          +1

                                          У вас async/await код больно страшный. К примеру, в функции checkItems последние catch и then на одном уровне, хотя первое относится к fs.stat, а второе к Promise.all. Плюс использовать исключения для управления потоком не очень хорошо


                                          К тому же код в примерах не эквивалентный, в promise-версии файлы фильтруются и читаются сразу, а в async-версии сначала получается список файлов, потом читается по списку — отсюда два использования Promise.all


                                          У меня с async получилось так:


                                          async function readFile(file) {
                                              const stat = await promisify(fs.stat, [path.join(__dirname, file)]);
                                              if (!stat.isFile()) {
                                                  console.error("Not a file!");
                                                  return null;
                                              }
                                              return await promisify(fs.readFile, [file]);
                                          }
                                          
                                          async function main() {
                                              const items = await promisify(fs.readdir, [__dirname]);
                                              const buffersOrNull = await Promise.all(items.map(readFile));
                                              return buffersOrNull.filter(buffer => buffer !== null);
                                          }
                                            0
                                            Третий случай, замеченный за корпоративным блогом ua-hosting, когда они копипастят текст с другого ресурса.

                                            На этот раз это даже не перевод: http://mabp.kiev.ua/2015/12/24/pyramid-of-death/
                                              0
                                              Да, Таша с моего разрешения опубликовала статью, поскольку у меня не хватает кармы, что бы сделать это самостоятельно

                                              Как видите, я активно отвечаю на каменты, так что, пожалуйста, не надо делать из этого поста корпоративные войны
                                                –1
                                                Добрый день! Тогда это кросспост, который запрещён правилами ;-)
                                                  0
                                                  Добрый пятничный вечер :)
                                                  несовсем, это адаптированая, для широких масс, версия оригинала
                                                  http://pyha.ru/forum/topic/9319.1

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

                                            Самое читаемое