Promises were never meant to be used as booleans but that's effectively what isGood()
is doing. And here, we don't just mean resolving/rejecting a promise with a boolean value. We mean that the state of a promise conveys its state :
- pending == as yet unknown
- resolved == true
- rejected == false
Some might regard this as promise abuse, but it's good fun trying to exploit promises in this way.
Arguably the main issues concerning promises as booleans are :
- The promise representation of 'true' will take the success path and the promise representation of 'false' will take the fail path
- Promise libraries don't naturally allow for all the necessary boolean algebra - eg. NOT, AND, OR, XOR
Until this topic is better explored and documented, it will take imagination to overcome/exploit these features.
Let's try and solve the problem (with jQuery - I know it much better).
First let's write a more definite version of 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();
}
We are going to need a "NOT" method - something that swaps 'resolved' and 'rejected'. jQuery promises don't have a native inverter, so here's a function to do the job.
/*
* 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);
});
}
Now, a version of the question's findGoodNumber()
, but here exploiting the rewritten isGood()
and the invertPromise()
utility.
/*
* 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()));
}
}
And finally, the same calling routine (with slightly different data) :
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
It should be quite simple to convert the code back to Angular/$q.
Explanation
The else
clause of findGoodNumber()
is maybe less than obvious. The core of it is numbers.reduce(...)
, which builds a .then()
chain - effectively an asychronous scan of the numbers
array. This is a familiar async pattern.
In the absence of the two inversions, the array would be scanned until the first bad number was found and the resulting rejection would take the failure path (skipping the remainder of the scan and proceeding to the fail handler).
However, we want to find the first good number to take the "failure" path- hence the need for :
- the inner inversion: to convert reported "true" to "false" - forcing the rest of the scan to be skipped
- the outer inversion: to restore the original bloolean sense - "true" ends up as "true" and "false" ends up as "false".
You may need to mess around with the demo to better appreciate what's going on.
Conclusion
Yes, it's possible to solve the problem without recursion.
This solution is neither the simplest nor the most efficient, however it hopefully demonstrates the potential of promises' state to represent booleans and to implement asynchronous boolean algebra.
Alternative solution
findGoodNumber()
can be written without needing to invert by performing an "OR-scan", as follows :
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());
}
}
This is the jQuery equivalent of Bergi's solution.
DEMO
q.all
would have been more appropriate. However,shift
is expensive so I would consider not mutating the array and simply increment an index. Nothing in the function's name implies thatnumbers
will be mutated, so if you stick with that solution, at least make a copy of it withnumbers.slice()
. If you have many numbers (enough to fill the call stack), then go for an iterative approach using a stack. – Icbm