3
\$\begingroup\$

I am trying to emulate Java 8's predicate chaining in JavaScript.

So far it works, but is there a more efficient way to write this? Furthermore, I had to create get and run methods to return the predicate so that is can be passed into a filter function.

Update: I added an _unwrap method to unwrap the internal predicate function so that you can pass a Predicate to another Predicate.

const records = [
  { name: 'Andrew' , age: 21, position: 'Developer' },
  { name: 'Bill'   , age: 26, position: 'Developer' },
  { name: 'Chris'  , age: 45, position: 'Manager'   },
  { name: 'Dave'   , age: 30, position: 'Developer' },
  { name: 'Eric'   , age: 32, position: 'Manager'   }
];

class Predicate {
  constructor(predicateFn) {
    this.predicateFn = this._unwrap(predicateFn);
  }
  /* @private */ _unwrap(predicateFn) {
    return predicateFn instanceof Predicate ? predicateFn.get() : predicateFn;
  }
  and(predicateFn) {
    return new Predicate((item) => this.predicateFn(item) && this._unwrap(predicateFn)(item));
  }
  or(predicateFn) {
    return new Predicate((item) => this.predicateFn(item) || this._unwrap(predicateFn)(item));
  }
  not() {
    return new Predicate((item) => !this.predicateFn(item));
  }
  get() {
    return this.predicateFn;
  }
  run(arr) {
    return arr.filter(this.predicateFn);
  }
}

const display = ({ name, age, position }) => `${name} (${age}) [${position}]`;

const ageOver21AndDeveloper = new Predicate(({ age }) => age > 21)
  .and(({ position }) => position === 'Developer');

console.log(ageOver21AndDeveloper.run(records).map(display).join(', '));

const billOrManager = new Predicate(({ name }) => name === 'Bill')
  .or(({ position }) => position === 'Manager');

console.log(billOrManager.run(records).map(display).join(', '));

const notUnder30 = new Predicate(({ age }) => age < 30).not();

console.log(notUnder30.run(records).map(display).join(', '));

// Pass predicate object to another predicate.
console.log(billOrManager.or(ageOver21AndDeveloper).run(records).map(display).join(', '));
.as-console-wrapper { top: 0; max-height: 100% !important; }

\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

I believe you could retain the same level of flexibility without managing extra objects by using higher order functions. This also has the advantage of treating the predicates as functions and nothing more.

const records = [
  { name: 'Andrew' , age: 21, position: 'Developer' },
  { name: 'Bill'   , age: 26, position: 'Developer' },
  { name: 'Chris'  , age: 45, position: 'Manager'   },
  { name: 'Dave'   , age: 30, position: 'Developer' },
  { name: 'Eric'   , age: 32, position: 'Manager'   }
];

const display = ({ name, age, position }) =>
     `${name} (${age}) [${position}]`;

const not = condition => (...args) => !condition(...args);

const and = (...conditions) => (...args) =>
    conditions.reduce(
        (a,b)=>a&&b(...args),true
    )

const or = (...conditions) => (...args) =>
    conditions.reduce(
        (a,b)=>a||b(...args),false
    )

const ageOver21 = ({age}) => age > 21
const isDeveloper = ({position}) => position === 'Developer';
const ageOver21AndIsDeveloper = and(ageOver21,isDeveloper);

console.log(records.filter(ageOver21AndIsDeveloper).map(display).join(', '));

const billOrManager = or(
    ({name}) => name === 'Bill',
    ({position}) => position === 'Manager'
)

console.log(records.filter(billOrManager).map(display).join(', '));

const notUnder30 = not(({ age }) => age < 30)

console.log(records.filter(notUnder30).map(display).join(', '));

console.log(
    records.filter(
        or(billOrManager,ageOver21AndIsDeveloper)
    )
    .map(display).join(', ')
);

\$\endgroup\$
1
\$\begingroup\$

You could avoid the unwrap logic by emulating a callable object. This is done by creating a function and attaching members to it. Your predicate class will then boil down to this small chunk of code:

const predicate = fn => (
  Object.assign((...args) => fn(...args), {
    and: otherFn => predicate(value => fn(value) && otherFn(value)),
    or: otherFn => predicate(value => fn(value) || otherFn(value)),
    not: () => predicate(value => !fn(value)),
    run: arr => arr.filter(fn),
  })
);

In this example, I'm first taking the passed-in function, copying it with (...args) => fn(...args) (this is done so we don't modify the original parameter), then assigning a bunch of functions to it with Object.assign().

Full Example

const records = [
  { name: 'Andrew' , age: 21, position: 'Developer' },
  { name: 'Bill'   , age: 26, position: 'Developer' },
  { name: 'Chris'  , age: 45, position: 'Manager'   },
  { name: 'Dave'   , age: 30, position: 'Developer' },
  { name: 'Eric'   , age: 32, position: 'Manager'   }
];

const predicate = fn => (
  Object.assign((...args) => fn(...args), {
    and: otherFn => predicate(value => fn(value) && otherFn(value)),
    or: otherFn => predicate(value => fn(value) || otherFn(value)),
    not: () => predicate(value => !fn(value)),
    run: arr => arr.filter(fn),
  })
);

const display = ({ name, age, position }) => `${name} (${age}) [${position}]`;

const ageOver21AndDeveloper = predicate(({ age }) => age > 21)
  .and(({ position }) => position === 'Developer');

console.log(ageOver21AndDeveloper.run(records).map(display).join(', '));

const billOrManager = predicate(({ name }) => name === 'Bill')
  .or(({ position }) => position === 'Manager');

// In this example we're letting billOrManager get called directly instead of using the .run() function
console.log(records.filter(billOrManager).map(display).join(', '));

const notUnder30 = predicate(({ age }) => age < 30).not();

console.log(notUnder30.run(records).map(display).join(', '));

// Pass predicate object to another predicate.
console.log(billOrManager.or(ageOver21AndDeveloper).run(records).map(display).join(', '));

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.