DEV Community

Derek Enos
Derek Enos

Posted on

Predilection for Predicates

I recently wanted to query arrays of Javascript objects using a declarative syntax and happily took the opportunity to write some small functions.

Predicates

EQ (is equal to)

const EQ = x => y => x === y
Enter fullscreen mode Exit fullscreen mode

For anyone unfamiliar with ES6 arrow functions:

EQ is a function that accepts one argument (x) that returns another function that accepts one argument (y) which returns the result of evaluating x === y.

Here's the equivalent standard function def:

function EQ (x) {
  return function (y) {
    return x === y
  }
}
Enter fullscreen mode Exit fullscreen mode

IN (is included in)

const IN = (...xs) => x => xs.includes(x)
Enter fullscreen mode Exit fullscreen mode

IN is a function that accepts one or more arguments collected into an array (xs) that returns another function that accepts one argument (x) which returns the result of evaluating xs.includes(x).

Logical Operators

const NOT = pred => x => !pred(x)
Enter fullscreen mode Exit fullscreen mode
const AND = (...preds) => x => preds.reduce((acc, pred) => acc && pred(x), true)
Enter fullscreen mode Exit fullscreen mode
const OR = (...preds) => x => preds.reduce((acc, pred) => acc || pred(x), false)
Enter fullscreen mode Exit fullscreen mode

Doing Stuff

Filtering Scalar Arrays

const data = [ 1, 2, 1, 1, 3, 2, 2, 2 ]
Enter fullscreen mode Exit fullscreen mode

Get All the 1s

>> data.filter(EQ(1))
Array(3) [ 1, 1, 1 ]
Enter fullscreen mode Exit fullscreen mode

Get All the 1s and 2s

>> data.filter(IN(1, 2))
Array(7) [ 1, 2, 1, 1, 2, 2, 2 ]
Enter fullscreen mode Exit fullscreen mode
>> data.filter(OR(EQ(1), EQ(2)))
Array(7) [ 1, 2, 1, 1, 2, 2, 2 ]
Enter fullscreen mode Exit fullscreen mode

Filtering Arrays of Objects

The above EQ and IN predicate functions work great with scalar values (i.e. numbers, booleans, etc), but I needed something that works on objects:

const OBJ = spec => obj => Object.entries(spec).reduce((acc, [k, pred]) => acc && pred(obj[k]), true)
Enter fullscreen mode Exit fullscreen mode

OBJ accepts an object-type spec argument that maps key names to predicates.
For example, a spec value of:

{ isAdmin: EQ(true), active: EQ(true) }
Enter fullscreen mode Exit fullscreen mode

would match objects with isAdmin = true AND active = true. For performing logical ops other than AND, you can specify them separately and wrap them appropriately. For example, to do an OR query on these same property values:

OR( OBJ({ isAdmin: EQ(true) }), OBJ({ active: EQ(true) }) )
Enter fullscreen mode Exit fullscreen mode

Better may be to create an OBJ_OR or something but...moving on

Get some legit-looking data from JSONPlaceholder

const Todos = await (await fetch("https://jsonplaceholder.typicode.com/todos")).json()
Enter fullscreen mode Exit fullscreen mode

The returned array looks like:

[
  {
    "userId": 1,
    "id": 1,
    "title": "delectus aut autem",
    "completed": false
  },
  ...
]
Enter fullscreen mode Exit fullscreen mode

Find all uncompleted Todos from Users 1 and 2:

>> Todos.filter(OBJ({userId: IN(1, 2), completed: EQ(false)}))
Array(21) [ {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, … ]
Enter fullscreen mode Exit fullscreen mode

🎉

Optimizations omitted in favor of simplicity

  • Short-circuit AND and OR on first false or true respectively instead of iterating over the entire array of object entries.

  • Support implicit EQ for non-predicate-function object spec values, e.g. { isAdmin: true } would be interpreted as { isAdmin: EQ(true) }.

Discussion (1)