11

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;
}

我想知道是否有更好的(也许是非递归的)方式?

4

4 回答 4

9

我想知道是否有更好的方法?

是的。避免延迟反模式

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))
}
于 2014-08-12T12:16:29.330 回答
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 和最近的 When)所拥有的那样。

于 2014-08-12T12:25:29.277 回答
1

这是我的版本,只需使用 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;
  }
});
于 2014-08-12T13:41:15.163 回答
0

Promise 从未打算用作布尔值,但这实际上isGood()是在做的事情。在这里,我们不仅仅是指用布尔值解决/拒绝一个承诺。我们的意思是,一个承诺的状态传达了它的状态:

  • 待定 == 尚不清楚
  • 解决==真
  • 拒绝 == 假

有些人可能认为这是滥用承诺,但尝试以这种方式利用承诺很有趣。

可以说,关于 Promise 作为布尔值的主要问题是:

  • “真”的承诺表示将走成功路径,“假”的承诺表示将走失败路径
  • Promise 库自然不会允许所有必要的布尔代数 - 例如。非、与、或、异或

在更好地探索和记录该主题之前,克服/利用这些功能需要想象力。

让我们尝试解决这个问题(使用 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();
}

我们将需要一个“NOT”方法——交换“已解决”和“已拒绝”的方法。jQuery Promise 没有本地逆变器,所以这里有一个函数来完成这项工作。

/* 
 * 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's 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');
});

演示

将代码转换回 Angular/$q 应该非常简单。

解释

of的else子句findGoodNumber()可能不太明显。它的核心是numbers.reduce(...),它构建了一个.then()链 - 有效地对numbers阵列进行异步扫描。这是一种熟悉的异步模式。

在没有两次反转的情况下,将扫描数组,直到找到第一个错误数字,并且结果拒绝将采用失败路径(跳过扫描的其余部分并继续进行失败处理程序)。

但是,我们想找到第一个走“失败”路径的好数字——因此需要:

  • 内部反转:将报告的“真”转换为“假” - 强制跳过扫描的其余部分
  • 外部反转:恢复原始的布尔意义——“真”以“真”结束,“假”以“假”结束。

您可能需要弄乱演示以更好地了解正在发生的事情。

结论

是的,无需递归即可解决问题。

这个解决方案既不是最简单的也不是最有效的,但是它有希望展示 Promise 状态表示布尔值和实现异步布尔代数的潜力。

替代解决方案

findGoodNumber()可以通过执行“或扫描”来编写而无需反转,如下所示:

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());
    }
}

这是 Bergi 解决方案的 jQuery 等价物。

演示

于 2014-08-12T22:29:43.263 回答