Передача дополнительных параметров в функции высшего порядка
Рассмотрим этот пример:
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"]