Javascript: как итерации по массиву с помощью promises?

LIVE DEMO

Учитывая следующую функцию:

function isGood(number) {
  var defer = $q.defer();

  $timeout(function() {
    if (<some condition on number>) {
      defer.resolve();
    } else {
      defer.reject();
    }
  }, 100);

  return defer.promise;
}

и массив чисел (например, [3, 9, 17, 26, 89]), я бы хотел найти первый "хороший" номер. Я хотел бы иметь возможность сделать это:

var arr = [3, 9, 17, 26, 89];

findGoodNumber(arr).then(function(goodNumber) {
  console.log('Good number found: ' + goodNumber);
}, function() {
  console.log('No good numbers found');
});

Вот одна из возможных рекурсивных версий для реализации этого: DEMO

function findGoodNumber(numbers) {
  var defer = $q.defer();

  if (numbers.length === 0) {
    defer.reject();
  } else {
    var num = numbers.shift();

    isGood(num).then(function() {
      defer.resolve(num);
    }, function() {
      findGoodNumber(numbers).then(defer.resolve, defer.reject)
    });
  }

  return defer.promise;
}

Интересно, есть ли лучший (может быть, нерекурсивный) способ?

Ответ 1

Интересно, есть ли лучший способ?

Да. Избегайте отложенного антипаттера!

function isGood(number) {
  return $timeout(function() {
    if (<some condition on number>) {
      return number; // Resolve with the number, simplifies code below
    } else {
      throw new Error("…");
    }
  }, 100);
}
function findGoodNumber(numbers) {
  if (numbers.length === 0) {
    return $q.reject();
  } else {
    return isGood(numbers.shift()).catch(function() {
      return findGoodNumber(numbers);
    });
  }
}

может быть нерекурсивным?

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

function findGoodNumber(numbers) {
  return numbers.reduce(function(previousFinds, num) {
    return previousFinds.catch(function() {
      return isGood(num);
    });
  }, $q.reject());
}

Это, однако, менее эффективно, поскольку он всегда смотрит на все numbers. "Рекурсивная" версия будет оценивать ее лениво и выполнять только другую итерацию, если текущее число не было хорошим.

может быть быстрее?

Вы можете запускать все проверки isGood параллельно и ждать возвращения первого выполненного. В зависимости от того, что на самом деле делает isGood, и насколько хорошо это можно распараллеливать, это может быть "лучше". Однако это потенциально делает много ненужной работы; вы можете использовать библиотеку обещаний, которая поддерживает отмену.

Пример использования библиотеки Bluebird, которая имеет any вспомогательную функцию, предназначенную для этой задачи:

function findGoodNumber(numbers) {
  return Bluebird.any(numbers.map(isGood))
}

Ответ 2

Вот альтернативное решение с другой формой рекурсии:

function firstGood(arr){
    var i = 0;
    return $q.when().then(function consume(){
        if(i >= arr.length) return $q.reject(Error("No Number Found"));
        return isGood(arr[i++]).catch(consume);
    });
}

Это очень похоже на то, что у Bergi есть, и о лучшем, что вы можете получить, не реализуя Promise.reduce, как некоторые библиотеки (Bluebird и совсем недавно).

Ответ 3

это моя версия, просто используя функцию array.map

Демо

angular.module('MyApp', []).run(function($q, $timeout) {
  var arr = [3, 9, 17, 26, 89];

  findGoodNumber(arr).then(function(goodNumber) {
    console.log('Good number found: ' + goodNumber);
  }, function() {
    console.log('No good numbers found');
  });

  function findGoodNumber(numbers) {
    var defer = $q.defer();

    numbers.forEach(function(num){      
      isGood(num).then(function(){
        defer.resolve(num);
      });

    });

    return defer.promise;
  }

  function isGood(number) {
    var defer = $q.defer();

    $timeout(function() {
      if (number % 2 === 0) {
        defer.resolve();
      } else {
        defer.reject();
      }
    }, 1000);

    return defer.promise;
  }
});

Ответ 4

Promises никогда не предназначались для использования в качестве булевых, но эффективно, что делает isGood(). И здесь мы не просто подразумеваем разрешение/отказ от обещания с логическим значением. Мы имеем в виду, что состояние обещания передает свое состояние:

  • Ожидание == пока неизвестно
  • resolved == true
  • reject == false

Некоторые могут расценивать это как обещание, но это весело, пытаясь использовать promises таким образом.

Возможно, основными проблемами, относящимися к promises как булевыми, являются:

  • Представление обетования "истина" займет путь успеха, а представление обетования "false" примет путь отказа.
  • Библиотеки Promise естественно не допускают всю необходимую булеву алгебру - например. NOT, AND, OR, XOR

Пока эта тема не будет лучше изучена и не задокументирована, для ее преодоления/использования этих функций потребуется фантазия.

Попробуем решить эту проблему (с jQuery - я знаю это намного лучше).

Сначала напишите более определенную версию isGood():

/*
 * A function that determines whether a number is an integer or not
 * and returns a resolved/rejected promise accordingly.
 * In both cases, the promise is resolved/rejected with the original number.
 */ 
function isGood(number) {
    return $.Deferred(function(dfrd) {
        if(parseInt(number, 10) == number) {
            setTimeout(function() { dfrd.resolve(number); }, 100);//"true"
        } else {
            setTimeout(function() { dfrd.reject(number); }, 100);//"false"
        }
    }).promise();
}

Нам понадобится метод "НЕ" - что-то, что свопирует "разрешено" и "отклонено". jQuery promises не имеет встроенного инвертора, поэтому здесь функция выполняет задание.

/* 
 * A function that creates and returns a new promise 
 * whose resolved/rejected state is the inverse of the original promise,
 * and which conveys the original promise value.
 */ 
function invertPromise(p) {
    return $.Deferred(function(dfrd) {
        p.then(dfrd.reject, dfrd.resolve);
    });
}

Теперь версия вопроса findGoodNumber(), но здесь используется перезаписанная утилита isGood() и invertPromise().

/*
 * A function that accepts an array of numbers, scans them,
 * and returns a resolved promise for the first "good" number,
 * or a rejected promise if no "good" numbers are present.
 */ 
function findGoodNumber(numbers) {
    if(numbers.length === 0) {
        return $.Deferred.reject().promise();
    } else {
        return invertPromise(numbers.reduce(function(p, num) {
            return p.then(function() {
                return invertPromise(isGood(num));
            });
        }, $.when()));
    }
}

И, наконец, одна и та же процедура вызова (с немного разными данными):

var arr = [3.1, 9.6, 17.0, 26.9, 89];
findGoodNumber(arr).then(function(goodNumber) {
    console.log('Good number found: ' + goodNumber);
}, function() {
    console.log('No good numbers found');
});

DEMO

Достаточно просто преобразовать код обратно в Angular/$q.

Объяснение

Предложение else findGoodNumber() возможно меньше, чем очевидно. Ядром этого является numbers.reduce(...), который строит цепочку .then() - эффективно асинхронное сканирование массива numbers. Это знакомый асинхронный шаблон.

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

Тем не менее, мы хотим найти первое хорошее число, чтобы принять "путь отказа" - отсюда необходимость:

  • внутренняя инверсия: преобразовать сообщение "true" в "false" - принудительное пропущение остальной части сканирования
  • внешняя инверсия: восстановить исходное значение bloolean - "true" заканчивается как "true" и "false" заканчивается как "false".

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

Заключение

Да, можно решить проблему без рекурсии.

Это решение не является ни простым, ни самым эффективным, однако, мы надеемся, демонстрирует потенциал состояния promises 'для представления логических функций и для реализации асинхронной булевой алгебры.

Альтернативное решение

findGoodNumber() можно записать без необходимости инвертировать, выполнив "OR-scan", следующим образом:

function findGoodNumber(numbers) {
    if(numbers.length === 0) {
        return $.Deferred.reject().promise();
    } else {
        return numbers.reduce(function(p, num) {
            return p.then(null, function() {
                return isGood(num);
            });
        }, $.Deferred().reject());
    }
}

Это эквивалент jQuery решения Берги.

DEMO