nicholaswmin
nicholaswmin

Reputation: 22949

Passing additional parameters in higher-order functions

Consider this example:

const samples = ["foo", "bar"];

const excludeFoos = function(item) {
  return item !== "foo";
}

const foos = samples.filter(excludeFoos);

How can I pass an additional parameter in excludeFoos?

For example:

const samples = ["foo", "bar"];

const exclude = function(item, str) {
  return item !== str;
}

// obviously won't work but you get the point
const foos = samples.filter(exclude("foo"));
console.log(foos); // ["bar"]

Upvotes: 13

Views: 7983

Answers (6)

Mulan
Mulan

Reputation: 135207

Naming things

"If you have the name of a spirit, you have power over it." – Gerald Jay Sussman

Can you think of a better name for your exclude function? I know I can. It's known as notEqual. Simply knowing it as its true name makes it much more versatile when it comes to problem solving. "exclude" makes sense in the context of filtering an array, but somehow it makes less sense if we wanted to use the exclude function elsewhere.

if (exclude(a,b))
  console.log("a and b are not equal")

Functional programming is all about making functions as reusable as possible, so as we move forward, let's stick with

const notEqual = (x,y) => x !== y

Function.prototype.bind

Function.prototype.bind is used to bind values to function parameters. It's commonly used because it's been native since ECMAScript 5 – meaning you can accomplish your goal without adding any additional dependencies or making any changes to your existing code.

const notEqual = (x,y) => x !== y

const samples = ['foo', 'bar']

const foos = samples.filter(notEqual.bind(null, 'foo'))

console.log(foos) // ["bar"]

Partial Application

Partial application takes a function and some arguments and produces another function of smaller arity – arity is a fancy word for "the number of arguments a function takes"

Now that you're familiar with Function.prototype.bind, you already know partial application. The only difference is bind forces you to provide the context of a binding. Contexts are a bother in most functional programs, so sometimes it's easier to have a function that lets us partially apply without concerning ourselves with context.

const partial = (f, ...xs) => (...ys) => f(...xs, ...ys)

const notEqual = (x,y) => x !== y

const samples = ['foo', 'bar']

const foos = samples.filter(partial(notEqual, 'foo'))

console.log(foos) // ["bar"]

Currying

Currying, while similar to partial application, is another way to approach your problem. Currying takes a function of multiple arguments and transforms it into a sequence of unary functions – functions that take one argument each.

const notEqual = (x,y) => x !== y

const curry = f => x => y => f(x,y)

const samples = ['foo', 'bar']

const foos = samples.filter(curry(notEqual)('foo'))

console.log(foos) // ["bar"]

If you're having trouble seeing how this is different than partial application, note you won't see much of a difference until function arity is greater than two – See also: contrast currying with partial application.

As you can see, readability is starting to suffer a little bit. Instead of currying on the fly, if notEqual is under our control, we could define it in curried form from the start

const notEqual = x => y => x !== y

const samples = ['foo', 'bar']

const foos = samples.filter(notEqual('foo'))

console.log(foos) // ["bar"]

You may not have even noticed it, but partial (above) is defined in curried style!

Related: "What do multiple arrow functions mean in JavaScript?"

Currying is a massively powerful concept and useful in a wide variety of ways. You might say it's overkill for solving this single, isolated problem, and you'd be right. You'll really only start to see the benefits of currying when it is widely used in a program or language as it has a systemic effect – and ultimately, it provides abstraction over function arity itself.

const apply = f => x => f (x)

const notEqual = x => y => x !== y

const filter = f => xs => xs.filter(apply(f))

const notFoo = filter(notEqual('foo'))

const samples = ['foo', 'bar']

console.log(notFoo(samples)); // ["bar"]

Final Remarks

There's a lot of options available to you and you might be wondering which is the "correct" one to choose. If you're looking for a silver bullet, you'll be sad to learn there isn't one. As with everything there are trade-offs.

I find partial/procedural application to be an indispensable tool, and therefore I try to write all of my JavaScript functions in fully curried form. That way I avoid dropping calls to partial and curry all over my program. The consequence of this is the code ends up looking a little foreign, at first – comparison functorround-robinmake anything you wanthigher-order generators and DIY iteratorsid generatorgeneric function repetitionmerge/flatten arraycustom iteration

Not all parts of your programs are fully under your control tho, right? Of course you're probably using some external dependencies and it's unlikely that they're going to have the perfect functional interface you're looking for. In such a case, you'll end up using partial and curry to interface with other code that you cannot change.

Lastly, look at some of the functional libraries out there like folktalke or Ramda. I don't recommend either for beginner functional programmers, but something worth looking into after you cut your teeth.

Upvotes: 37

LetterEh
LetterEh

Reputation: 26696

Here's one for you:

There are a couple of answers that talk about curry, and partial-application.

And that's a great direction.

But once you really get higher-order functions, you can make this stuff really clean and easy to work with.

const curry = (f, ...initialArgs) => (...extraArgs) => {
  const args = [...initialArgs, ...extraArgs];
  return args.length >= f.length ? f(...args) : curry(f, ...args);
};

So what does that do?
It lets you pass in a function, and gives you a function. Until you have passed in enough arguments to run the function, it's going to keep passing you another function that expects more arguments.

What good is that?

const multiply = curry((x, y) => x * y);
const double = multiply(2);
const triple = multiply(3);

double(2); // 4
triple(9); // 27

Now it's really easy to define something like your test.

const notEqual = curry((test, x) => test !== x);

// you could do it like this, to reuse `notFoo`
const notFoo = notEqual("foo");
samples.filter(notFoo);

// you could do it like this, if you don't need `notFoo`
samples.filter(notEqual("foo"));

But wait! There's more!

const filter = curry((predicate, array) => array.filter(predicate));

const removeFoos = filter(notEqual("foo"));
removeFoos(samples);
removeFoos(items);
removeFoos(otherStuff);

Now I have a function that filters out foos and I can just pass it arrays whenever I feel like it.

Last one for now:

const compose = (...fs) => x => fs.reduceRight((x, f) => f(x), x);

Instead of writing

h(g(f(x)));

Compose lets me write

const hgf = compose(h, g, f);
hgf(x);
hgf(y);
hgf(z);

// it's read from right to left
const tto = compose(three, two, one);

// or from bottom to top
const tsf = compose(
  third,
  second,
  first
);

// because it runs like
y = third(second(first(x)));

So now, let's try something wild...

// lib functions (Ramda would work fine)
const map = curry((transform, array) => array.map(transform));
const reduce = curry((summarize, seed, array) => 
  array.reduce(summarize, seed));
const flatMap = curry((transform, array) =>
  array.map(transform).reduce((a, b) => a.concat(b), []));

// business functions
const castToEmployee = personData => new Employee(personData);
const isWorking = ({ active }) => active;
const removeSuperiors = curry((user, employee) =>
  employee.role <= user.role);

const customEmployeeCriteria = (criteria, employee) => { /*...*/ };
const removeDuplicates = (arr, employee) =>
  arr.some(person => person.id === employee.id)
    ? arr
    : arr.concat(employee);

Library Code

const performCustomSearch = searchCriteria => 
  filter(cutomEmployeeCriteria(searchCriteria));

const getAuthorizedEmployeeList = currentUser =>
  filter(removeSuperiors(currentUser));

const buildEmployees = compose(
  filter(isWorking),
  map(castToEmployee),
);

const cleanResults = compose(
  filter(removeBrokenItem),
  map(removePrivateMembers),
  reduce(removeDuplicates, []),
);

const handleEmployeeRequest = (currentUser, searchCriteria) => compose(
  cleanResults,
  performCustomSearch(searchCriteria),
  getAuthorizedEmployeeList(currentUser),
  buildEmployees
);

API Code

//(maybe /employees/?search={...}&token=123)
router.get("/employees", (req, res) => {
  PersonService.getAll()
    .then(handleEmployeeRequest(req.user, req.query.search))
    .then(filteredEmployees => res.json(filteredEmployees));
});

And we're done.
Easy as pie.

Upvotes: 2

גלעד ברקן
גלעד ברקן

Reputation: 23955

Here's another version with a primitive curry function:

const samples = ["foo", "bar"];

const exclude = function(item,str) {
  return item !== str;
}

function curry(func){
  return function(var1){
    return function(var2){
      return func(var1,var2); 
    };
  };
}

console.log(curry(exclude)('foo')('bar'));  // true
console.log(samples.filter(curry(exclude)('foo')));  // ["bar"]

Upvotes: 0

Keith Nicholas
Keith Nicholas

Reputation: 44288

you want to curry your function like so :-

const samples = ["foo", "bar"];

const exclude = function(s) {
  return item => item !== s;
}

const foos = samples.filter(exclude("foo"));
console.log(foos)

excludeFoos returns a function for filtering. Many functional languages auto curry functions for you so you can do partial application

Note, it is easier to embrace something like Ramda for js which is built around these concepts and allows you to pipe collections / filters etc

Upvotes: 3

Pabs123
Pabs123

Reputation: 3435

You can use bind() to create a new function with the bound params;

//you can replace the param with anything you like, null is for the context
var excludeFoos = exclude.bind(null,"foos")
const foos = samples.filter(excludeFoos);

Live example here

Upvotes: 6

fafl
fafl

Reputation: 7385

With ES6:

const foos = samples.filter(x => exclude(x, "foos"));

another option would be to use bind(), but I find it difficult to read:

const foos = samples.filter(exclude.bind(null, "foos"))

Upvotes: 4

Related Questions