В чем разница между карри и частичным применением?


438

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

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

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

Ответы:


256

Карринг - это преобразование одной функции из 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 to 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

40
Вы говорите, что частичное применение - это когда вы каррируете функцию и используете некоторые, но не все полученные функции?
SpoonMeiser

9
более или менее да. Если вы предоставите только подмножество аргументов, вы получите функцию, которая принимает остальные аргументы
Mark Cidade,

1
Будет ли изменение функции f (a, b, c, d) на g (a, b) считаться частичным применением? Или это только применительно к функциям карри? Извините за боль, но я ожидаю здесь четкого ответа.
SpoonMeiser

2
@Mark: Полагаю, это лишь одна из тех концепций, которая вызывает во мне педантизм, но обращение к авторитетным источникам мало что дает для удовлетворения, поскольку все они, похоже, указывают друг на друга. Википедия вряд ли то, что я считаю авторитетным источником, но я понимаю, что трудно найти что-то еще. Достаточно сказать, что я думаю, что мы оба знаем то, о чем мы говорим, и силу этого, независимо от того, можем ли мы согласиться (или не согласиться) с особенностями родного языка! :) Спасибо Марк!
Джейсон Бантинг

5
@JasonBunting, Относительно вашего первого комментария, то, о чем вы говорили, - это отказ . Curry принимает функцию с несколькими аргументами в качестве входных данных и возвращает цепочку функций с 1 аргументом в качестве выходных данных. Удаление карри принимает цепочку 1-аргументных функций в качестве входных данных и возвращает мультиаргументную функцию в качестве выходных. Как подробно
описано

165

Самый простой способ увидеть, чем они отличаются - рассмотреть реальный пример . Давайте предположим, что у нас есть функция, 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
    
  • Каррирование функции 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
    

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

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


25
Частичное приложение преобразует функцию из n-aryв (x - n)-ary, каррируя из n-aryв n * 1-ary. Частично примененная функция имеет уменьшенную область применения (приложения), то Add7есть менее выразительна, чем Add. С другой стороны, функция карри столь же выразительна, как и исходная функция.
Боб

4
Я считаю, что более отличительной чертой является то, что когда мы карри f (x, y, z) => R, мы получаем f (x), который возвращает g (y) => h (z) => R, каждый из которых потребляет один аргумент; но когда мы частично применяем f (x, y, z) как f (x), мы получаем g (y, z) => R, то есть с двумя аргументами. Если бы не эта черта, мы могли бы сказать, что каррирование похоже на частичное применение к 0 аргументам, оставляя все аргументы несвязанными; однако в действительности функция f (), частично примененная к 0 аргументам, является функцией, потребляющей 3 аргумента одновременно, в отличие от карри f ().
Максим Гумеров

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

2
Что означает комментарий f2(7)(5) is just a syntactic shortcut? (Я знаю очень мало.) f2Уже не содержит / "знать о" 7?
Зак Межеевский,

@Pacerier, есть ли curryгде-нибудь реализация (не думаю, что она есть functools)
alancalvitti

51

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

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

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

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

let double2 z = multiply 2 z

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

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

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


33

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

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

Имейте в виду, что эта пара терминов очень запутана.


5
Первая ссылка о различиях. Вот еще один, который я нашел полезным: bit.ly/CurringVersusPartialApplication
Джейсон Бантинг

5
Каррирование связано с кортежами (превращение функции, которая принимает аргумент кортежа, в функцию, которая принимает n отдельных аргументов, и наоборот). Частичное применение - это возможность применить функцию к некоторым аргументам, получив новую функцию для оставшихся аргументов. Это легко запомнить, если вы думаете, что карри == делать с кортежами.
Дон Стюарт

9
@Jon ссылки, которые вы разместили, информативны, но будет лучше расширить свой ответ и добавить сюда дополнительную информацию.
Захир Ахмед


11
Не могу поверить, что вы получили 20 голосов за пару ссылок и признание, которое вы на самом деле не знаете разницы между карри и частичным применением. Хорошо сыграно, сэр.
AlienWebguy

16

Я ответил на это в другой теме 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

ради карри.


1
Вы бы сказали, что карри - это частный случай частичного применения?
SpoonMeiser

1
@SpoonMeiser, нет, каррирование не является частным случаем частичного применения: частичное применение функции с 2 входами не то же самое, что каррирование функции. См. Stackoverflow.com/a/23438430/632951 .
Pacerier

10

Curry - это функция одного аргумента, которая принимает функцию 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)

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

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

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

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


8

Разницу между карри и частичным применением лучше всего проиллюстрировать на следующем примере 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), это не частичное применение в действительности.

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


«частично примененная функция сразу выдаст результат при вызове» - это не правильно, не так ли? когда я частично применяю функцию, это выражение возвращает функцию, а не «результат». Хорошо, вы, вероятно, имели в виду, что эта последняя функция, когда вызывается с остальными аргументами, возвращает результат, в отличие от копания одного шага в карри. Но на самом деле никто не говорит, что вы должны указать все оставшиеся аргументы: вы можете частично применить результат частичного применения, и это снова будет функцией, а не «результатом»
Максим Гумеров

6

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

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

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


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

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

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


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

(в JavaScript)

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

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

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

отказ

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


4

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

Как частичное применение, так и каррирование включают предоставление аргументов функции, возможно, не все сразу. Довольно канонический пример - добавление двух чисел. В псевдокоде (фактически 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 (общее количество аргументов в окончательной оценке) частичной реализации приложения, которая может объединить два моих описания в запутанный беспорядок ... но в этот момент эти два метода в значительной степени взаимозаменяемы.


3

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

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


3

Я могу ошибаться, поскольку у меня нет достаточного опыта в теоретической математике или функциональном программировании, но из моего краткого опыта в 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» и вставьте в мой код, чтобы увидеть скомпилированную версию, которая может (надеюсь) иметь больше смысла. Спасибо!


2

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

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

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

Карри - это когда вы определяете функцию.

Частичное применение - это когда вы вызываете функцию.

Приложение говорит по математике для вызова функции.

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


1

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

public static <A,B,X> Function< B, X > partiallyApply( BiFunction< A, B, X > aBiFunction, A aValue ){
    return b -> aBiFunction.apply( aValue, b );
}

public static <A,X> Supplier< X > partiallyApply( Function< A, X > aFunction, A aValue ){
    return () -> aFunction.apply( aValue );
}

public static <A,B,X> Function<  A, Function< B, X >  > curry( BiFunction< A, B, X > bif ){
    return a -> partiallyApply( bif, a );
}

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

Если вы хотите копировать и вставлять, ниже будет шумнее, но удобнее работать, так как типы более мягкие:

public static <A,B,X> Function< ? super B, ? extends X > partiallyApply( final BiFunction< ? super A, ? super B, X > aBiFunction, final A aValue ){
    return b -> aBiFunction.apply( aValue, b );
}

public static <A,X> Supplier< ? extends X > partiallyApply( final Function< ? super A, X > aFunction, final A aValue ){
    return () -> aFunction.apply( aValue );
}

public static <A,B,X> Function<  ? super A,  Function< ? super B, ? extends X >  > curry( final BiFunction< ? super A, ? super B, ? extends X > bif ){
    return a -> partiallyApply( bif, a );
}

Следующее дало мне понимание: «Таким образом, каррирование дает вам функцию с одним аргументом для создания функций, где частичное приложение создает функцию-обертку, которая жестко кодирует один или несколько аргументов».
Роланд

0

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

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

(+) :: Int -> Int -> Int

Теперь, как вы превращаете это в функцию, которая принимает один аргумент? Вы обманываете, конечно!

plus :: (Int, Int) -> Int

Обратите внимание, что плюс теперь принимает один аргумент (который состоит из двух вещей). Супер!

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

(uncurry (+)) (1,2)

Так что же такое частичное применение функции? Это другой способ превратить функцию с двумя аргументами в функцию с одним аргументом. Это работает по-другому, хотя. Опять же, давайте возьмем (+) в качестве примера. Как мы можем превратить его в функцию, которая принимает один Int в качестве аргумента? Мы обманываем!

((+) 0) :: Int -> Int

Это функция, которая добавляет ноль к любому Int.

((+) 1) :: Int -> Int

добавляет 1 к любому Int. И т. Д. В каждом из этих случаев (+) «частично применяется».

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.