What Functional-pattern Is This? If Any?
Solution 1:
You could use the where
function in Ramda to test against a spec object describing your predicates. Your code could then build the spec object dynamically according to the passed config.
https://ramdajs.com/docs/#where
Example from the Ramda docs:
//pred ::Object->Booleanconstpred=R.where({a:R.equals('foo'),b:R.complement(R.equals('bar')),x:R.gt(R.__,10),y:R.lt(R.__,20)});pred({a:'foo',b:'xxx',x:11,y:19});//=>truepred({a:'xxx',b:'xxx',x:11,y:19});//=>falsepred({a:'foo',b:'bar',x:11,y:19});//=>falsepred({a:'foo',b:'xxx',x:10,y:19});//=>falsepred({a:'foo',b:'xxx',x:11,y:20});//=>false
To elaborate, you could "build" the spec object by having a set of functions that return a new spec with an additional predicate, e.g.:
functionsetMinIncome(oldSpec, minIncome) {
return R.merge(oldSpec, {income: R.gt(R.__, minIncome)})
}
Solution 2:
Functional programming is less about patterns and more about laws. Laws allow the programmer to reason about their programs like a mathematician can reason about an equation.
Let's look at adding numbers. Adding is a binary operation (it takes two numbers) and always produces another number.
1 + 2 = 32 + 1 = 3
1 + (2 + 3) = 6(1 + 2) + 3 = 6
((1 + 2) + 3) + 4 = 10(1 + 2) + (3 + 4) = 101 + (2 + 3) + 4 = 101 + (2 + (3 + 4)) = 10
We can add numbers in any order and still get the same result. This property is associativity and it forms the basis of the associative law.
Adding zero is somewhat interesting, or taken for granted.
1 + 0 = 10 + 1 = 1
3 + 0 = 30 + 3 = 3
Adding zero to any number will not change the number. This is known as the identity element.
These two things, (1) an associative binary operation and (2) an identity element, make up a monoid.
If we can ...
- encode your predicates as elements of a domain
- create a binary operation for the elements
- determine the identity element
... then we receive the benefits of belonging to the monoid category, allowing us to reason about our program in an equational way. There's no pattern to learn, only laws to uphold.
1. Making a domain
Getting your data right is tricky, even more so in a multi-paradigm language like JavaScript. This question is about functional programming though so functions would be a good go-to.
In your program ...
build() {
varfnPredicate = (p) => true;
if (typeof config.minIncome == 'number') {
fnPredicate = (p) =>fnPredicate(p) && config.minIncome <= p.income;
}
if (typeof config.member == 'boolean') {
fnPredicate = (p) =>fnPredicate(p) && config.member === p.member;
}
// .. continue to support more predicateparts.
},
... we see a mixture of the program level and the data level. This program is hard-coded to understand only an input that may have these specific keys (minIncome
, member
) and their respective types (number
and boolean
), as well the comparison operation used to determine the predicate.
Let's keep it really simple. Let's take a static predicate
item.name === "Sally"
If I wanted this same predicate but compared using a different item, I would wrap this expression in a function and make item
a parameter of the function.
constnameIsSally = item =>
item.name === "Sally"console .log
( nameIsSally ({ name: "Alice" }) // false
, nameIsSally ({ name: "Sally" }) // true
, nameIsSally ({ name: "NotSally" }) // false
, nameIsSally ({}) // false
)
This predicate is easy to use, but it only works to check for the name Sally. We repeat the process by wrapping the expression in a function and make name
a parameter of the function. This general technique is called abstraction and it's used all the time in functional programming.
constnameIs = name => item =>
item.name === name
const nameIsSally =
nameIs ("Sally")
const nameIsAlice =
nameIs ("Alice")
console .log
( nameIsSally ({ name: "Alice" }) // false
, nameIsSally ({ name: "Sally" }) // true
, nameIsAlice ({ name: "Alice" }) // true
, nameIsAlice ({ name: "Sally" }) // false
)
As you can see, it doesn't matter that the expression we wrapped was already a function. JavaScript has first-class support for functions, which means they can be treated as values. Programs that return a function or receive a function as input are called higher-order functions.
Above, our predicates are represented as functions which take a value of any type (a) and produce a boolean. We will denote this as a -> Boolean
. So each predicate is an element of our domain, and that domain is all functions a -> Boolean
.
2. The Binary Operation
We'll do the exercise of abstraction one more time. Let's take a static combined predicate expression.
p1 (item) && p2 (item)
I can re-use this expression for other items by wrapping it in a function and making item
a parameter of the function.
const bothPredicates = item =>
p1 (item) && p2 (item)
But we want to be able to combine any predicates. Again, we wrap the expression we want to re-use in an function then assign parameter(s) for the variable(s), this time for p1
and p2
.
constand = (p1, p2) => item =>
p1 (item) && p2 (item)
Before we move on, let's check our domain and ensure our binary operation and
is correct. The binary operation must:
- take as input two (2) elements from our domain (
a -> Boolean
) - return as output an element of our domain
- the operation must be associative: f(a,b) == f(b,a)
Indeed, and
accepts two elements of our domain p1
and p2
. The return value is item => ...
which is a function receiving an item
and returns p1 (item) && p2 (item)
. Each is a predicate that accepts a single value and returns a Boolean. This simplifies to Boolean && Boolean
which we know is another Boolean. To summarize, and
takes two predicates and returns a new predicate, which is precisely what the binary operation must do.
constand = (p1, p2) => item =>
p1 (item) && p2 (item)
constnameIs = x => item =>
item.name === x
constminIncome = x => item =>
x <= item.incomeconst query =
and
( nameIs ("Alice")
, minIncome (5)
)
console .log
( query ({ name: "Sally", income: 3}) // false
, query ({ name: "Alice", income: 3 }) // false
, query ({ name: "Alice", income: 7 }) // true
)
3. The Identity Element
The identity element, when added to any other element, must not change the element. So for any predicate p
and the predicate identity element empty
, the following must hold
and (p, empty) == pand (empty, p) == p
We can represent the empty predicate as a function that takes any element and always returns true
.
constand = (p1, p2) => item =>
p1 (item) && p2 (item)
constempty = item =>
trueconstp = x =>
x > 5console .log
( and (p, empty) (3) === p (3) // true
, and (empty, p) (3) === p (3) // true
)
Power of Laws
Now that we have a binary operation and an identity element, we can combine an arbitrary amount of predicates. We define sum
which plugs our monoid directly into reduce
.
// --- predicate monoid ---constand = (p1, p2) => item =>
p1 (item) && p2 (item)
constempty = item =>
trueconstsum = (...predicates) =>
predicates .reduce (and, empty) // [1,2,3,4] .reduce (add, 0) // --- individual predicates ---constnameIs = x => item =>
item.name === x
constminIncome = x => item =>
x <= item.incomeconstisTeenager = item =>
item.age > 12 && item.age < 20// --- demo ---const query =
sum
( nameIs ("Alice")
, minIncome (5)
, isTeenager
)
console .log
( query ({ name: "Sally", income: 8, age: 14 }) // false
, query ({ name: "Alice", income: 3, age: 21 }) // false
, query ({ name: "Alice", income: 7, age: 29 }) // false
, query ({ name: "Alice", income: 9, age: 17 }) // true
)
The empty sum predicate still returns a valid result. This is like the empty query that matches all results.
const query =
sum ()
console .log
( query ({ foo: "bar" }) // true
)
Free Convenience
Using functions to encode our predicates makes them useful in other ways too. If you have an array of items, you could use a predicate p
directly in .find
or .filter
. Of course this is true for predicates created using and
and sum
too.
const p =
sum (pred1, pred2, pred3, ...)
const items =
[ { name: "Alice" ... }
, { name: "Sally" ... }
]
const firstMatch =
items .find (p)
const allMatches =
items .filter (p)
Make it a Module
You don't want to define globals like add
and sum
and empty
. When you package this code, use a module of some sort.
// Predicate.jsconstadd = ...
const empty = ...
const sum = ...
const Predicate =
{ add, empty, sum }
export default Predicate
When you use it
import { sum } from'./Predicate'
const query =
sum (...)
const result =
arrayOfPersons .filter (query)
Quiz
Notice the similarity between our predicate identity element and the identity element for &&
T && ? == T? && T == TF && ? == F? && F == F
We can replace all ?
above with T
and the equations will hold. Below, what do you think the identity element is for ||
?
T || ? == T? || T == TF || ? == F? || F == F
What's the identity element for *
, binary multiplication?
n * ? = n? * n = n
How about the identity element for arrays or lists?
concat (l, ?) == lconcat (?, l) == l
Having Fun?
I think you'll enjoy contravariant functors. In the same arena, transducers. There's a demo showing how to build a higher-level API around these low-level modules too.
Solution 3:
This is the Builder design pattern. Although it's changed in a more functional approach but the premise stays the same - you have an entity that collects information via .map()
(more traditionally it's .withX()
which correspond to setters) and executes all the collected data producing a new object .build()
.
To make this more recognisable, here is a more Object Oriented approach that still does the same thing:
classPerson {
constructor(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
toString() {
return`I am ${this.firstName}${this.lastName} and I am ${this.age} years old`;
}
}
classPersonBuilder {
withFirstName(firstName) {
this.firstName = firstName;
returnthis;
}
withLastName(lastName) {
this.lastName = lastName;
returnthis;
}
withAge(age) {
this.age = age;
returnthis;
}
build() {
returnnewPerson(this.firstName, this.lastName, this.age);
}
}
//make builderconst builder = newPersonBuilder();
//collect data for the object construction
builder
.withFirstName("Fred")
.withLastName("Bloggs")
.withAge(42);
//build the object with the collected dataconst person = builder.build();
console.log(person.toString())
Solution 4:
I'd stick to a simple array of (composed) predicate functions and a reducer of either
And
(f => g => x => f(x) && g(x)
), seeded withTrue
(_ => true
).Or
(f => g => x => f(x) || g(x)
), seeded withFalse
(_ => false
).
For example:
constTrue = _ => true;
constFalse = _ => false;
constOr = (f, g) => x =>f(x) || g(x);
Or.seed = False;
constAnd = (f, g) => x =>f(x) && g(x);
And.seed = True;
constFilter = (fs, operator) => fs.reduce(operator, operator.seed);
const oneOrTwo =
Filter([x => x === 1, x => x === 2], Or);
const evenAndBelowTen =
Filter([x => x % 2 === 0, x => x < 10], And);
const oneToHundred = Array.from(Array(100), (_, i) => i);
console.log(
"One or two",
oneToHundred.filter(oneOrTwo),
"Even and below 10",
oneToHundred.filter(evenAndBelowTen)
);
You can even create complicated filter logic by nesting And
/Or
structures:
constTrue = _ => true;
constFalse = _ => false;
constOr = (f, g) => x =>f(x) || g(x);
Or.seed = False;
constAnd = (f, g) => x =>f(x) && g(x);
And.seed = True;
constFilter = (fs, operator) => fs.reduce(operator, operator.seed);
constmod = x => y => y % x === 0;
const oneToHundred = Array.from(Array(100), (_, i) => i);
console.log(
"Divisible by (3 and 5), or (3 and 7)",
oneToHundred.filter(
Filter(
[
Filter([mod(3), mod(5)], And),
Filter([mod(3), mod(7)], And)
],
Or
)
)
);
Or, with your own example situation:
constcomp = (f, g) => x =>f(g(x));
constgt = x => y => y > x;
consteq = x => y => x === y;
constprop = k => o => o[k];
constAnd = (f, g) => x =>f(x) && g(x);
constTrue = _ => true;
constFilter = (fs) => fs.reduce(And, True);
const richMemberFilter = Filter(
[
comp(gt(200000), prop("income")),
comp(eq(true), prop("member"))
]
);
console.log(
"Rich members:",
data().filter(richMemberFilter).map(prop("firstName"))
);
functiondata() {
return [
{ firstName: 'Jesper', lastName: 'Jensen', income: 120000, member: true },
{ firstName: 'Jane', lastName: 'Johnson', income: 230000, member: true },
{ firstName: 'John', lastName: 'Jackson', income: 230000, member: false }
];
};
Post a Comment for "What Functional-pattern Is This? If Any?"