Skip to content Skip to sidebar Skip to footer

What Functional-pattern Is This? If Any?

I find myself over and over again, writing code like this, and is thinking. There must be a known pattern for this, but plowing through documentation of different functional libs l

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 ...

  1. encode your predicates as elements of a domain
  2. create a binary operation for the elements
  3. 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:

  1. take as input two (2) elements from our domain (a -> Boolean)
  2. return as output an element of our domain
  3. 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 with True (_ => true).
  • Or (f => g => x => f(x) || g(x)), seeded with False (_ => 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?"