it-roy-ru.com

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

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

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

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

395
SpoonMeiser

Карринг - это преобразование одной функции 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))} с переданным -в значениях х и у 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
229
Mark Cidade

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

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

147
Pacerier

Примечание: это было взято из F # Основы отличной вводной статьи для разработчиков .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 вычисляется, он использует свой аргумент вместе с частично примененным значением для создания результата.

48
dodgy_coder

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

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

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

27
Jon Skeet

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

ради карри.

12
Ji Han

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)

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

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

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

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

6
Roland

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

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

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

6 === partial(2, 3);

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

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

5
gsklee

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

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

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


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

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

Реальная разница видна, когда функция имеет более 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');

Отказ

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

2
Kamafeather

Я могу ошибаться, так как у меня нет достаточного опыта в теоретической математике или функциональном программировании, но из моего краткого опыта в 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
sunny-mittal

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

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

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

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

2
Taoufik Dachraoui

Здесь есть и другие замечательные ответы, но я считаю, что этот пример (согласно моему пониманию) в 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 );
}
1
ArtB

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

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

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

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

plus :: (Int, Int) -> Int

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

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

(uncurry (+)) (1,2)

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

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

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

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

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

0
nomen

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

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

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

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

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

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

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

0
Brennan Cheung