Передача дополнительных параметров в функции высшего порядка

Рассмотрим этот пример:

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

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

const foos = samples.filter(excludeFoos);

Как я могу передать дополнительный параметр в excludeFoos?

Например:

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"]

6 ответов

Решение

Называть вещи

"Если у вас есть имя духа, у вас есть власть над ним". - Джеральд Джей Суссман

Можете ли вы придумать лучшее название для вашего exclude функционировать? Я знаю что могу. Это известно как notEqual, Простое знание его как его истинного имени делает его гораздо более универсальным, когда дело доходит до решения проблем."исключить" имеет смысл в контексте фильтрации массива, но почему-то это имеет меньше смысла, если мы хотим использовать exclude функционировать в другом месте.

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

Функциональное программирование - это создание максимально возможного многократного использования функций, поэтому по мере продвижения вперед давайте придерживаться

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

Function.prototype.bind

Function.prototype.bind используется для привязки значений к параметрам функции. Он обычно используется, потому что он является родным начиная с ECMAScript 5 - это означает, что вы можете достичь своей цели без добавления каких-либо дополнительных зависимостей или внесения каких-либо изменений в существующий код.

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

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

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

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

Частичное применение

Частичное приложение принимает функцию и некоторые аргументы и создает другую функцию меньшей арности - arity - причудливое слово для "числа аргументов, которые принимает функция"

Теперь, когда вы знакомы с Function.prototype.bind, вы уже знаете частичное применение. Единственная разница bind заставляет вас предоставить контекст привязки. Контексты мешают в большинстве функциональных программ, поэтому иногда проще иметь функцию, которая позволяет нам частично применять, не касаясь контекста.

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"]

Карринг

Карринг, хотя и похож на частичное применение, является еще одним способом решения вашей проблемы. Карринг принимает функцию из нескольких аргументов и преобразует ее в последовательность унарных функций - функций, каждый из которых принимает один аргумент.

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"]

Если вам трудно понять, чем это отличается от частичного применения, обратите внимание, что вы не увидите большой разницы, пока арность функции не станет больше двух. См. Также: контрастное каррирование с частичным применением.

Как видите, читаемость начинает немного ухудшаться. Вместо того, чтобы карри на лету, если notEqual находится под нашим контролем, мы могли бы определить его в карри форме с самого начала

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

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

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

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

Возможно, вы даже не заметили, но partial (выше) определяется в стиле карри!

Связанный: "Что означают множественные функции стрелок в JavaScript?"

Карри - это чрезвычайно мощная концепция, полезная для самых разных целей. Вы можете сказать, что решение этой единственной изолированной проблемы излишне, и вы были бы правы. Вы действительно только начнете видеть преимущества каррирования, когда он широко используется в программе или языке, поскольку он имеет системный эффект - и, в конечном счете, он обеспечивает абстракцию самой функции функции.

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"]

Заключительные замечания

Вам доступно множество вариантов, и вам может быть интересно, какой из них "правильный". Если вы ищете серебряную пулю, вам будет грустно узнать, что ее нет. Как и во всем, есть компромиссы.

Я считаю частичное / процедурное приложение незаменимым инструментом, и поэтому я пытаюсь написать все свои функции JavaScript в полностью каррированном виде. Таким образом я избегаю отбрасывать звонки partial а также curry по всей моей программе. Следствием этого является то, что сначала код выглядит немного чуждым - функтор сравненияциклический переборсоздайте все, что вы хотитегенераторы высшего порядка и самодельные итераторыгенератор идентификаторовповторение универсальной функциимассив слияния / сглаживанияпользовательский итерация

Не все части ваших программ полностью находятся под вашим контролем, правда? Конечно, вы, вероятно, используете некоторые внешние зависимости, и вряд ли у них будет идеальный функциональный интерфейс, который вы ищете. В таком случае вы в конечном итоге будете использовать partial а также curry взаимодействовать с другим кодом, который вы не можете изменить.

Наконец, посмотрите на некоторые из функциональных библиотек, таких как фолктальке или Рамда. Я не рекомендую ни для начинающих функциональных программистов, но кое-что, на что стоит обратить внимание после того, как вы порежете зубы.

Ты можешь использовать bind() создать новую функцию со связанными параметрами;

//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);

Живой пример здесь

С ES6:

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

другой вариант будет использовать bind(), но мне трудно читать:

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

Вы хотите карри свою функцию так:

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

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

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

excludeFoos возвращает функцию для фильтрации. Многие функциональные языки автоматически выполняют функции карри, так что вы можете сделать частичное применение

Обратите внимание, что легче использовать что-то вроде Ramda для js, которое построено вокруг этих концепций и позволяет передавать коллекции / фильтры и т. Д.

Вот один для вас:

Есть несколько ответов, которые говорят о карри и частичном применении.

И это отличное направление.

Но как только вы действительно получите функции более высокого порядка, вы можете сделать этот материал действительно чистым и простым в работе.

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

Так, что это делает?
Это позволяет вам передать функцию и дает вам функцию. Пока вы не передадите достаточно аргументов для запуска функции, она будет продолжать передавать вам другую функцию, которая ожидает больше аргументов.

Что хорошего в этом?

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

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

Теперь действительно легко определить что-то вроде вашего теста.

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"));

Но ждать! Есть еще кое-что!

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

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

Теперь у меня есть функция, которая отфильтровывает foos, и я могу просто передавать ее в любое время, когда захочу.

Последний на данный момент:

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

Вместо того чтобы писать

h(g(f(x)));

Compose позволяет мне писать

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)));

Итак, давайте попробуем что-то дикое...

// 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);

Библиотечный код

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-код

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

И мы сделали.
Проще простого.

Вот еще одна версия с примитивом curry функция:

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"]

Другие вопросы по тегам