Как вы выполняете любую функцию javascript произвольной арности?

Скажем, у меня есть некоторая функция:

function g(a,b,c){ return a + b + c }

И я хотел бы превратить его в форму "карри" (в кавычках, поскольку это не совсем карри как таковое):

function h(a,b,c){

    switch(true){

        case (a !== undefined && b !== undefined && c !== undefined):
            return a + b + c

        case (a !== undefined && b !== undefined && c === undefined): 
            return function(c){ return a + b + c }

        case (a !== undefined && b == undefined  && c === undefined ):
            return function(b,c){
                return (c === undefined) ? function(c){ return a + b + c } : a + b + c
            }

        default:
            return h

    }

}

Вышеупомянутая форма имеет поведение частичной привязки, которое я хочу:

h(1)     -> h(b,c)
h(1,2)   -> h(c)
h(1,2,3) -> 6
h()      -> h(a,b,c)

Теперь я хотел бы автоматизировать этот процесс в некоторой универсальной функции curry, чтобы при любой некаррированной функции (и, возможно, ее количестве параметров) генерировалась вышеуказанная функция. Но я не совсем уверен, как это реализовать.

В качестве альтернативы, если бы следующая форма могла быть создана автоматически, это было бы также интересно:

function f(a,b,c){
    return function(a){ return function(b){ return function(c){ return a + b + c }}}
}

Хотя привязка f выглядит так:

f(1)(2)(3) = 6

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

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


person xiaolingxiao    schedule 27.09.2013    source источник
comment
lo-dash уже разобрался с этим. См. lodash.com/docs#curry. Ты можешь просто использовать это?   -  person Chris Montgomery    schedule 27.09.2013
comment
Вы не можете, действительно, в общем случае — аргументы в JavaScript не требуются. Что вы будете делать с необязательными аргументами?   -  person Ry-♦    schedule 27.09.2013
comment
@minitech Вы можете отличить пропущенные аргументы от указанных аргументов, проверив, являются ли аргументы undefined. Таким образом, хотя JS не проверяет во время компиляции, что список аргументов достаточно длинный, достаточно легко проверить это во время выполнения и вести себя по-разному в зависимости от того, сколько аргументов указано.   -  person Rory O'Kane    schedule 27.09.2013
comment
@РориО'Кейн: function dIsOptional(a, b, c, d) { … } var c = curry(dIsOptional); c(1, 2, 3); // ???   -  person Ry-♦    schedule 27.09.2013
comment
@ChrisMontgomery потрясающая библиотека, я использовал подчеркивание и не знал, что это существует   -  person xiaolingxiao    schedule 28.09.2013
comment
@ chibro2 Было бы неплохо сказать мне, что не так с моим ответом, чтобы его можно было принять ;) Вы его смотрели?   -  person plalx    schedule 28.09.2013
comment
@plalx привет, чувак, я принял твой ответ, потому что он определенно работает. Но вместо этого я использовал _.curry из lo-dash по предложению Криса, так как в библиотеке так много полезных вещей.   -  person xiaolingxiao    schedule 28.09.2013


Ответы (5)


Я считаю, что вы могли бы просто использовать Function.prototype.bind. Это дает вам всю необходимую гибкость, независимо от того, хотите ли вы сразу получить результат функции или просто вставить другое значение в аргументы, пока вы не решите выполнить.

function sum() {
    return [].reduce.call(arguments, function (c, n) {
        return c + n;
    });
}

sum(1, 2); //3

var sum2 = sum.bind(null, 1, 2);

sum2(); //3

var sum3 = sum2.bind(null, 3);

sum3(); //6

Вы также можете использовать вспомогательную функцию, например:

function curry(fn) {
    var c = curry.bind(this, fn = fn.bind.apply(fn, [this].concat([].slice.call(arguments, 1))));

    c.exec = fn;

    return c;
}

curry(sum, 1, 2)(3)(4, 5)(6, 7, 8).exec(); //36

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

var sumOnePlus = curry(sum, 1);

sumOnePlus.exec(2); //3;
sumOnePlus.exec(3); //4;
person plalx    schedule 27.09.2013
comment
@Bergi Что ты думаешь об этом? - person plalx; 27.09.2013
comment
Второй выглядит очень хорошо (но код должен быть менее сложным), первый (bind), к сожалению, просто частичное приложение без каррирования. - person Bergi; 28.09.2013

Вот моя попытка:

function curry(fn, len) {
    if (typeof len != "number")
        len = fn.length; // getting arity from function
    return function curried() {
        var rlen = len - arguments.length;
        if (rlen <= 0) // then execute now
            return fn.apply(this, arguments);
        // else create curried, partially bound function:
        var bargs = [this]; // arguments for `bind`
        bargs.push.apply(bargs, arguments);
        return curry(fn.bind.apply(fn, bargs), rlen);
    };
}

Это не частичное приложение (что легко сделать в JS с bind метод), но действительно функциональный каррирование. Работает с любыми функциями произвольной, но фиксированной арности. Для вариативных функций вам понадобится другой триггер выполнения, возможно, когда аргументы больше не передаются, или метод exec, как в ответе @plalx.

person Bergi    schedule 28.09.2013
comment
Действительно ли функциональное каррирование более полезно, чем просто частичное применение (через bind) в JavaScript? Могу ли я сделать что-то с помощью этой функции curry, чего я не мог сделать с помощью bind? Или, если я не могу сделать ничего нового с curry, могу ли я сделать что-то более понятное, чем с bind (т.е. синтаксис лучше)? У меня есть приблизительное представление о разнице между каррированием и частичным применением; Сейчас я пытаюсь понять, является ли это полезным различием для фактического написания JS. - person Noah Freitas; 15.08.2015
comment
@NoahFreitas Да, это другое, см. также en.wikipedia.org/wiki/. Если вы частично применяете функции, вы передаете аргументы для привязки к вызову partial. Если вы каррируете функцию, вы не передаете никаких аргументов, а только возвращаете функцию, которая автоматически частично применяет себя при необходимости. Различие важно, какой из них более полезен в реальном js, зависит от вашей проблемы. - person Bergi; 16.08.2015
comment
Спасибо за ответ. Это могло бы помочь мне лучше понять, если бы вы могли привести пример, в котором вы решили использовать эту функцию curry вместо Function.prototype.bind. Я также понимаю, что это, вероятно, требует слишком многого для комментария. - person Noah Freitas; 16.08.2015
comment
@NoahFreitas: Вы можете взглянуть на Ramda. Все их функции каррированы. - person Bergi; 16.08.2015

Как насчет такого:

function makeLazy(fn) {
    var len = fn.length;
    var args = [];
    return function lazy() {
        args.push.apply(args, arguments);
        if (args.length < len) {
            return lazy;
        } else {
            return fn.apply(this, args);
        }
    }
}

function f(a,b,c) { return a + b + c; }

var lazyF = makeLazy(f);
lazyF(1)(2)(3); // 6

var lazyF = makeLazy(f);
lazyF(1,2)(3); // 6

Если вам нужна повторно используемая функция (думаю, я не могу точно сказать, что вы хотите), то это сработает:

function makeCurry(fn) {
    return function curry() {
        var args = [].slice.call(arguments);
        return function() {
            return fn.apply(this, args.concat.apply(args, arguments));
        };
    }
}


function f(a,b,c) { return a + b + c; }

var curryF = makeCurry(f);
var addOneTwoAnd = curryF(1,2);

addOneTwoAnd(3); // 6
addOneTwoAnd(6); // 9
person user2736012    schedule 27.09.2013
comment
На самом деле это не сработает, потому что тогда вы разделяете args на все функции, полученные в результате данного вызова makeLazy. Например, если f принимает два аргумента, то var g = makeLazy(f); g(1); g(2); g(3) эквивалентно f(1, 2); f(1, 2, 3), что совсем не интуитивно понятно. - person ruakh; 27.09.2013
comment
@ruakh: Да, теперь я понимаю, что ты имеешь в виду. Я неправильно истолковал то, что было после OP. Сначала это выглядело как ленивая оценка, но теперь я вижу, что OP просто хочет каррировать любое количество аргументов. Обновлен, но оставил оригинальный ответ для любопытных. - person user2736012; 27.09.2013
comment
@ruakh Что вы думаете о моей реализации curry? - person plalx; 27.09.2013

Пожалуйста, проверьте библиотеку curry.

Он может превратить любую функцию в карри независимо от количества аргументов.

Пример:

> var curry = require('curry');
undefined
> var add = curry(function(a, b, c, d, e) { return a + b + c + d + e; });
undefined
> add(1)
[Function]
> add(1,2,3,4,5)
15
> add(1,2)(3,4)(5)
15
>
person Tyler Long    schedule 05.07.2016

Метод bind() для функции позволяет привязать this внутри функции, а также привязать дополнительные параметры. Итак, если вы передадите null для параметра this, вы можете использовать bind() для каррирования параметров в функцию.

function g(a,b,c){ return a + b + c }

var g_1 = g.bind(null, 1);
console.log(g_1(2, 3)); // Prints 6

var g_1_2 = g.bind(null, 1, 2);
console.log(g_1_2(3)); // Prints 6

См. функция Javascript bind() для получения подробной информации и интерактивных примеров использования bind() для привязки параметров.

person nkron    schedule 27.09.2013
comment
Мне кажется, или у вас такой же ответ, как у меня? - person plalx; 27.09.2013