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; }
2 Answers 2
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(', ')
);
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(', '));