1

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 like Ramda. I can't quite find a match. What should I use?

var arrayOfPersons = [{ firstName: 'Jesper', lastName: 'Jensen', income: 120000, member: true }/* .... a hole lot of persons */];

function createPredicateBuilder(config) {

  return {

    build() {
      var fnPredicate = (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.
    },

    map(newConfig) {
      return createPredicateBuilder({ ...config, ...newConfig });
    }
  };
}

var predicateBuilder = createPredicateBuilder({});

// We collect predicates
predicateBuilder = predicateBuilder.map({ minIncome: 200000 });
// ...
predicateBuilder = predicateBuilder.map({ member: false });

// Now we want to query...
console.log(arrayOfPersons.filter(predicateBuilder.build()));

I create a builder instance, and calls to map, creates a new instance returning in an object with build/map methods. The state is captured in the functions scope. Sometime in the future, I want to get my collected function (or result).

I think this is FP, but what is this pattern, and is there any libs that makes it easyer?

Is my oop-inspired naming of things (builder/build) blinding me?

Jesper Jensen
  • 835
  • 8
  • 16
  • I'm not an expert in FP, but I think you could work with a Monad instead of your predicateBuilder. – Jonathan van de Veen Dec 11 '18 at 07:47
  • It is not composing the function, but more collecting the parts and storring them in the scope. So besides composing fpPredicate with monads, how would you use monads to collect? – Jesper Jensen Dec 11 '18 at 07:56
  • The builder pattern is an OOP design pattern and AFAIK it is used to mitigate one of the drawbacks of product types like objects. Product types are hierarchical that is, you can only add information and wind up with a lot of `null` values and a lot of constrcutors handling all the combinations of member variables. Sure, you can avoid `this`. And you can reduce mutations by relying on some functions' call stack, but essentially, it is still an OOP pattern and there is no name for it in FP. –  Dec 11 '18 at 09:13

4 Answers4

3

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 -> Boolean
const pred = 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}); //=> true
pred({a: 'xxx', b: 'xxx', x: 11, y: 19}); //=> false
pred({a: 'foo', b: 'bar', x: 11, y: 19}); //=> false
pred({a: 'foo', b: 'xxx', x: 10, y: 19}); //=> false
pred({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.:

function setMinIncome(oldSpec, minIncome) {
  return R.merge(oldSpec, {income: R.gt(R.__, minIncome)})
}
Jonas Høgh
  • 10,358
  • 1
  • 26
  • 46
  • I see where you are going, and I looked at R.where and R.memoize, but it is not quite the same. It is 'building' the predicate, based on parameters, my exampel is doing. – Jesper Jensen Dec 11 '18 at 08:11
  • Yes I see what you are getting at. Only thing missing, is the ability to 'overwrite' a spec. My 'builder' will have the spec-parts in scope, and overwrite them, but I think I will try some things out the path you are pointing. – Jesper Jensen Dec 11 '18 at 08:45
  • How to update the object in a functional way (that is, return a new one) is of course a topic in its own right. E.g. you could use lenses – Jonas Høgh Dec 11 '18 at 08:55
3

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 = 3
2 + 1 = 3

1 + (2 + 3) = 6
(1 + 2) + 3 = 6

((1 + 2) + 3) + 4 = 10
(1 + 2) + (3 + 4) = 10
1 + (2 + 3) + 4 = 10
1 + (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 = 1
0 + 1 = 1

3 + 0 = 3
0 + 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() {
  var fnPredicate = (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.

const nameIsSally = 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.

const nameIs = 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.

const and = (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.

const and = (p1, p2) => item =>
  p1 (item) && p2 (item)

const nameIs = x => item =>
  item.name === x
  
const minIncome = x => item =>
  x <= item.income

const 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) == p
and (empty, p) == p

We can represent the empty predicate as a function that takes any element and always returns true.

const and = (p1, p2) => item =>
  p1 (item) && p2 (item)
  
const empty = item =>
  true
  
const p = x =>
  x > 5
  
console .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 ---
const and = (p1, p2) => item =>
  p1 (item) && p2 (item)

const empty = item =>
  true
  
const sum = (...predicates) =>
  predicates .reduce (and, empty)  // [1,2,3,4] .reduce (add, 0) 

// --- individual predicates ---
const nameIs = x => item =>
  item.name === x

const minIncome = x => item =>
  x <= item.income

const isTeenager = 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.js
const add = ...

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 == T
F && ? == 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 == T
F || ? == 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, ?) == l
concat (?, 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.

Mulan
  • 129,518
  • 31
  • 228
  • 259
2

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:

class Person {
  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`;
  }
}

class PersonBuilder {
  withFirstName(firstName) { 
    this.firstName = firstName;
    return this;
  }
  withLastName(lastName) {
    this.lastName = lastName;
    return this;
  }
  withAge(age) {
    this.age = age;
    return this;
  }
  
  build() {
    return new Person(this.firstName, this.lastName, this.age);
  }
}

//make builder
const builder = new PersonBuilder();

//collect data for the object construction
builder
  .withFirstName("Fred")
  .withLastName("Bloggs")
  .withAge(42);

//build the object with the collected data
const person = builder.build();

console.log(person.toString())
VLAZ
  • 26,331
  • 9
  • 49
  • 67
  • I want builder design pattern in FP - so no state mutation and reference to this. – Jesper Jensen Dec 11 '18 at 08:05
  • 1
    @JesperJensen this is a more "traditional" implementation of the Builder pattern. And by "traditional", I mean Object Oriented, as it would be easier to identify it from other literature. There is nothing stopping you from doing exactly what you're doing, I'd consider it the same pattern although it's not using the exact same syntax as OO code. The point is to do a delayed construction of *something* (it need not be a class instance) which gives you the opportunity to collect data before that. – VLAZ Dec 11 '18 at 08:09
0

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:

const True = _ => true;
const False = _ => false;

const Or = (f, g) => x => f(x) || g(x);
Or.seed = False;

const And = (f, g) => x => f(x) && g(x);
And.seed = True;

const Filter = (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:

const True = _ => true;
const False = _ => false;

const Or = (f, g) => x => f(x) || g(x);
Or.seed = False;

const And = (f, g) => x => f(x) && g(x);
And.seed = True;

const Filter = (fs, operator) => fs.reduce(operator, operator.seed);
  
const mod = 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:

const comp = (f, g) => x => f(g(x));

const gt = x => y => y > x;
const eq = x => y => x === y;
const prop = k => o => o[k];

const And = (f, g) => x => f(x) && g(x);
const True = _ => true;

const Filter = (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"))
);

function data() { 
  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 }
  ]; 
};
user3297291
  • 22,592
  • 4
  • 29
  • 45