В чем разница между карри и частичным применением? - PullRequest
394 голосов
/ 20 октября 2008

Я нередко вижу в Интернете различные жалобы на то, что примеры карри других людей не карри, а на самом деле лишь частичное применение.

Я не нашел достойного объяснения того, что такое частичное приложение или чем оно отличается от карри. Похоже, что существует общая путаница с эквивалентными примерами, описываемыми как карри в одних местах и ​​частичное применение в других.

Может ли кто-нибудь дать мне определение как терминов, так и деталей их различия?

Ответы [ 14 ]

229 голосов
/ 20 октября 2008

Curry - это преобразование одной функции из n аргументов в n функций с одним аргументом в каждой. Учитывая следующую функцию:

function f(x,y,z) { z(x(y));}

При карри становится:

function f(x) { lambda(y) { lambda(z) { z(x(y)); } } }

Чтобы получить полное применение функции f (x, y, z), вам нужно сделать следующее:

f(x)(y)(z);

Многие функциональные языки позволяют писать f x y z. Если вы вызываете только f x y или f (x) (y) , тогда вы получаете частично примененную функцию - возвращаемое значение является закрытием lambda(z){z(x(y))} с переданными значениями x и y до f(x,y).

Одним из способов использования частичного применения является определение функций как частичных применений обобщенных функций, таких как fold :

function fold(combineFunction, accumulator, list) {/* ... */}
function sum     = curry(fold)(lambda(accum,e){e+accum}))(0);
function length  = curry(fold)(lambda(accum,_){1+accum})(empty-list);
function reverse = curry(fold)(lambda(accum,e){concat(e,accum)})(empty-list);

/* ... */
@list = [1, 2, 3, 4]
sum(list) //returns 10
@f = fold(lambda(accum,e){e+accum}) //f = lambda(accumulator,list) {/*...*/}
f(0,list) //returns 10
@g = f(0) //same as sum
g(list)  //returns 10
146 голосов
/ 03 мая 2014

Самый простой способ увидеть, чем они отличаются, - рассмотреть реальный пример . Давайте предположим, что у нас есть функция Add, которая принимает 2 числа в качестве входных данных и возвращает число в качестве выходных, например, Add(7, 5) возвращает 12. В этом случае:

  • Частичное применение функции Add со значением 7 даст нам новую функцию в качестве вывода. Эта функция сама принимает на вход 1 число и выводит число. Как таковой:

    Partial(Add, 7); // returns a function f2 as output
    
                     // f2 takes 1 number as input and returns a number as output
    

    Итак, мы можем сделать это:

    f2 = Partial(Add, 7);
    f2(5); // returns 12;
           // f2(7)(5) is just a syntactic shortcut
    
  • Curry функция Add даст нам новую функцию в качестве вывода. Эта функция сама принимает 1 число в качестве входа и выводит , а еще одну новую функцию. Эта третья функция затем принимает 1 число в качестве ввода и возвращает число в качестве вывода. Как таковой:

    Curry(Add); // returns a function f2 as output
    
                // f2 takes 1 number as input and returns a function f3 as output
                // i.e. f2(number) = f3
    
                // f3 takes 1 number as input and returns a number as output
                // i.e. f3(number) = number
    

    Итак, мы можем сделать это:

    f2 = Curry(Add);
    f3 = f2(7);
    f3(5); // returns 12
    

Другими словами, «карри» и «частичное применение» - это две совершенно разные функции. Curry занимает ровно 1 вход, тогда как частичное применение требует 2 (или более) входов.

Даже если они оба возвращают функцию в качестве вывода, возвращаемые функции имеют совершенно разные формы, как показано выше.

48 голосов
/ 04 мая 2012

Примечание: это взято из Основы F # отличной вводной статьи для разработчиков .NET, посвященных функциональному программированию.

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

let multiply x y = x * y    
let double = multiply 2
let ten = double 5

Сразу же вы должны увидеть поведение, отличное от большинства императивные языки. Второе утверждение создает новую функцию вызывается double, передавая один аргумент функции, которая принимает два. Результатом является функция, которая принимает один аргумент int и выдает тот же вывод, как если бы вы назвали умножение с х, равным 2 и у равно этому аргументу. С точки зрения поведения, это так же, как это код:

let double2 z = multiply 2 z

Часто люди ошибочно говорят, что умножение каррируется в двойное число. Но это только несколько верно. Функция умножения карри, но это происходит, когда он определен, потому что функции в F # каррируются дефолт. Когда двойная функция создана, она более точно скажем, что функция умножения применяется частично.

Функция умножения на самом деле представляет собой последовательность из двух функций. Первый функция принимает один аргумент int и возвращает другую функцию, эффективно связывает х с определенным значением. Эта функция также принимает аргумент int, который вы можете рассматривать как значение для привязки к y. После вызывая эту вторую функцию, x и y оба связаны, поэтому результат произведение х и у, как определено в теле double.

Для создания двойника первая функция в цепочке умножения Функции оцениваются для частичного применения умножения. Результирующий функция имеет имя double. Когда double оценивается, он использует его аргумент вместе с частично примененным значением для создания результат.

27 голосов
/ 20 октября 2008

Интересный вопрос. После небольшого поиска «Приложение с частичными функциями не каррирует» дало лучшее объяснение, которое я нашел. Я не могу сказать, что практическая разница особенно очевидна для меня, но тогда я не эксперт по ФП ...

Еще одна полезная страница (которую, признаюсь, я еще не полностью прочитал) - это «Каррирование и частичное применение с замыканиями Java» .

Похоже, эта пара слов сильно запутана, заметьте.

12 голосов
/ 11 октября 2012

Я ответил на это в другой теме https://stackoverflow.com/a/12846865/1685865. Короче говоря, частичное применение функции - это исправление некоторых аргументов данной функции с несколькими переменными, чтобы получить другую функцию с меньшим числом аргументов, в то время как Curry - это превращение функции из N аргументов в унарную функцию, которая возвращает унарную функцию ... [Пример Карри показано в конце этого поста.]

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

(пример карри)

На практике не просто написать

lambda x: lambda y: lambda z: x + y + z

или эквивалентный JavaScript

function (x) { return function (y){ return function (z){ return x + y + z }}}

вместо

lambda x, y, z: x + y + z

ради карри.

6 голосов
/ 30 августа 2017

Curry - это функция аргумента one , которая принимает функцию f и возвращает новую функцию h. Обратите внимание, что h принимает аргумент из X и возвращает функцию , которая сопоставляет Y с Z:

curry(f) = h 
f: (X x Y) -> Z 
h: X -> (Y -> Z)

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

part(f, 2) = g
f: (X x Y) -> Z 
g: Y -> Z

Путаница возникает из-за того, что с функцией с двумя аргументами выполняется следующее равенство:

partial(f, a) = curry(f)(a)

Обе стороны выдают одну и ту же функцию с одним аргументом.

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

Разница также в поведении, в то время как каррирование рекурсивно преобразует всю исходную функцию (один раз для каждого аргумента), частичное применение - это просто замена в один шаг.

Источник: Википедия Карри .

5 голосов
/ 27 мая 2013

Разницу между карри и частичным применением лучше всего проиллюстрировать на следующем примере JavaScript:

function f(x, y, z) {
    return x + y + z;
}

var partial = f.bind(null, 1);

6 === partial(2, 3);

Частичное применение приводит к функции меньшей арности; в приведенном выше примере f имеет арность 3, в то время как partial имеет арность только 2. Что более важно, частично примененная функция будет сразу возвращать результат после вызова , а не другой функцией вниз по цепочке карри. Так что если вы видите что-то вроде partial(2)(3), это не частичное применение в действительности.

Дальнейшее чтение:

2 голосов
/ 09 июля 2018

Простой ответ

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

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


Простые подсказки

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

Реальная разница видна, когда функция имеет более 2 аргументов.


Простой е (с) (образец)

(в Javascript)

function process(context, success_callback, error_callback, subject) {...}

зачем всегда передавать аргументы, такие как context и callbacks, если они всегда будут одинаковыми? Просто свяжите некоторые значения для функции

processSubject = _.partial(process, my_context, my_success, my_error)

и позвоните на subject1 и foobar с

processSubject('subject1');
processSubject('foobar');

Удобно, не так ли? ?

С карри вам нужно будет передавать один аргумент за раз

curriedProcess = _.curry(process);
processWithBoundedContext = curriedProcess(my_context);
processWithCallbacks = processWithBoundedContext(my_success)(my_error); // note: these are two sequential calls

result1 = processWithCallbacks('subject1');
// same as: process(my_context, my_success, my_error, 'subject1');
result2 = processWithCallbacks('foobar'); 
// same as: process(my_context, my_success, my_error, 'foobar');

Ответственность

Я пропустил все академические / математические объяснения. Потому что я этого не знаю. Может быть, это помогло ?

2 голосов
/ 13 ноября 2017

У меня часто возникал этот вопрос во время обучения, и с тех пор его задавали много раз. Самый простой способ, которым я могу описать разницу, состоит в том, что оба они одинаковы :) Позвольте мне объяснить ..., очевидно, есть различия.

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

add = (x, y) => x + y

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

addOneC = curry(add, 1)
addOneP = partial(add, 1)

Теперь их использование понятно:

addOneC(2) #=> 3
addOneP(2) #=> 3

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

curriedAdd = curry(add) # notice, no args are provided
addOne = curriedAdd(1) # returns a function that can be used to provide the last argument
addOne(2) #=> returns 3, as we want

partialAdd = partial(add) # no args provided, but this still returns a function
addOne = partialAdd(1) # oops! can only use a partially applied function once, so now we're trying to add one to an undefined value (no second argument), and we get an error

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

curriedAdd = curry(add)
curriedAdd()()()()()(1)(2) # ugly and dumb, but it works

partialAdd = partial(add)
partialAdd()()()()()(1)(2) # second invocation of those 7 calls fires it off with undefined parameters

Надеюсь, это поможет!

ОБНОВЛЕНИЕ: Некоторые языки или реализации lib позволят вам передать arity (общее количество аргументов в окончательной оценке) частичной реализации приложения, которая может объединить мои два описания в запутанный беспорядок ... но в этот момент две техники в значительной степени взаимозаменяемы.

2 голосов
/ 07 декабря 2015

Я могу ошибаться, поскольку у меня нет достаточного опыта в теоретической математике или функциональном программировании, но из моего краткого знакомства с FP кажется, что карринг превращает функцию из N аргументов в N функций один аргумент, тогда как частичное применение [на практике] лучше работает с переменными функциями с неопределенным числом аргументов. Я знаю, что некоторые примеры в предыдущих ответах не поддаются этому объяснению, но это помогло мне больше всего отделить понятия. Рассмотрим этот пример (написанный на CoffeeScript для краткости, приношу свои извинения, если он еще больше смущает, но, если необходимо, попросите разъяснений):

# partial application
partial_apply = (func) ->
  args = [].slice.call arguments, 1
  -> func.apply null, args.concat [].slice.call arguments

sum_variadic = -> [].reduce.call arguments, (acc, num) -> acc + num

add_to_7_and_5 = partial_apply sum_variadic, 7, 5

add_to_7_and_5 10 # returns 22
add_to_7_and_5 10, 11, 12 # returns 45

# currying
curry = (func) ->
  num_args = func.length
  helper = (prev) ->
    ->
      args = prev.concat [].slice.call arguments
      return if args.length < num_args then helper args else func.apply null, args
  helper []

sum_of_three = (x, y, z) -> x + y + z
curried_sum_of_three = curry sum_of_three
curried_sum_of_three 4 # returns a function expecting more arguments
curried_sum_of_three(4)(5) # still returns a function expecting more arguments
curried_sum_of_three(4)(5)(6) # returns 15
curried_sum_of_three 4, 5, 6 # returns 15

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

Опять же, это мой взгляд из того, что я прочитал. Если кто-то не согласен, я был бы признателен за комментарий, а не за немедленное понижение. Кроме того, если CoffeeScript трудно читать, посетите coffeescript.org, нажмите «попробовать coffeescript» и вставьте в мой код, чтобы увидеть скомпилированную версию, которая может (надеюсь) иметь больше смысла. Спасибо!

...