How to support promised predicates in Array find

I’m building ACL support in NodeJS. For example, I’ll allow to write:

{[ .can-example | 1.hilite(=javascript=) ]}

For any given action, my ´Can´ function must check a list of permissions. If one matches, then the action ´Can´ asks for is granted, otherwise (no permission exists) it’s denied. Of course I’m only interested in the first permission to match, and to allow for optimizations, I want permissions to be checked in the order I provide.

It’s easy to see that my problem is solved by the Array.prototype.find method. The problem I have, though, is that it only works with immediate predicates, but my checks can entail both immediate and promised predicates. For example, I allow predicates to access the database.

I googled my problem and found this StackOverflow page. Bergi’s answer gives both recursive and non-recursive solutions. (Side note. There was a time when recursive was opposed to iterative. With promises, that’s no longer the case. In fact, the non-recursive solution is a chain of ´catch´ handlers. An iteration is used to build the chain but promises themselves, throwing exceptions, control the iteration.) Benjamin Gruenbaum’s answer gives a recursive solution.

Here are their issues.

  1. The predicate is hacky because it signals a ´false´ by throwing an exception.
  2. The promise management is hacky because (a) it must cater for the predicate with ´.catch()´, and (b) it signals the “not found” outcome with ´reject()´.
  3. The promise management and the predicate are very coupled.
  4. The contract is different from that of the Array.prototype.find method.

So I came up with this one.

{[ .ArrayFind | 1.hilite(=javascript=) ]}

Apart from being a global function instead of an Array instance method, the contract is exactly the same as that of the Array.prototype.find method.

  1. The only hack I used is to immediately exit when an element is found instead of continuing until the end of the ´.then()´ chain. But how I implemented it is both robust (as in Robustness) and hidden (as in Information Hiding). To make sure I do not mistake a rightful exception with my hack, I throw my own fake exception which is a wrapper around the found element. Thus, my fake exception is caught by the last ´.catch()´ and the element is returned.
  2. The predicate can be both immediate or a promise, thanks to ´Promise.resolve(Predicate…)´. If it’s immediate, it can throw an exception if it has to, not if it doesn’t hold true. If it’s a promise, it can reject() if it has to, not if it doesn’t hold true.

Examples

Here are some examples.

{[ .predicates | 1.hilite(=javascript=) ]}

The result is a promise

Here is how ArrayFind compares to Array.prototype.find when no exceptions are thrown:

[1,13,5,4,7].find(ImmediatePredicate)
VM834:3 -- 1
VM834:3 -- 13
VM834:3 -- 5
VM834:3 -- 4
4

ArrayFind([1,13,5,4,7], ImmediatePredicate)
VM834:3 -- 1
VM834:3 -- 13
VM834:3 -- 5
VM834:3 -- 4
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}

Of course the result is a Promise instead of the found element, but we can append additional handlers, like

ArrayFind([1,13,5,4,7], ImmediatePredicate)
  .then(function(result){
    console.log(result);
  })
  .catch(function(reason){
    console.warn(reason)
  })

VM834:3 -- 1
VM834:3 -- 13
VM834:3 -- 5
VM834:3 -- 4
VM880:4 4
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}

An exception makes the promise reject

Here is how ArrayFind compares to Array.prototype.find when an exception is thrown:

[1,3,5,4,7].find(ImmediatePredicate)
VM834:3 -- 1
VM834:3 -- 3
VM834:4 Uncaught dirty

ArrayFind([1,3,5,4,7], ImmediatePredicate)
  .then(function(result){
    console.log(result);
  })
  .catch(function(reason){
    console.warn(reason)
  })

VM834:3 -- 1
VM834:3 -- 3
VM925:7 dirty
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}

Immediate and promised predicates work equally well

Here you can see how ArrayFind supports at the same time immediate and promised predicates:

ArrayFind([1,13,5,4,7], MixedPredicate)
  .then(function(result){
    console.log(result);
  })
  .catch(function(reason){
    console.warn(reason)
  })

VM834:3 -- 1
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
VM834:12 -- 13
VM834:3 -- 5
VM834:3 -- 4
VM955:4 4


ArrayFind([1,3,5,4,7], MixedPredicate)
  .then(function(result){
    console.log(result);
  })
  .catch(function(reason){
    console.warn(reason)
  })

VM834:3 -- 1
VM834:3 -- 3
VM974:7 dirty
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}


ArrayFind([1,12,5,4,7], MixedPredicate)
  .then(function(result){
    console.log(result);
  })
  .catch(function(reason){
    console.warn(reason)
  })

VM834:3 -- 1
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
VM834:12 -- 12
VM975:7 dirty

You can’t appreciate from the output above, but the immediate predicate really outputs immediately. 🙂

Also notice that in the second to last example the warned ´dirty´ comes from the immediate predicate because 3 < 10, while in the last example it comes from the promised predicate because 12 >= 10.

Leave a Reply

Your email address will not be published. Required fields are marked *