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

StackOverflow https://stackoverflow.com/questions/218025

Вопрос

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

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

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

Это было полезно?

Решение

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

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

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

Другие советы

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

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

Примечание:это было взято из Основы 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

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

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

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

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

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

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

Я ответил на этот вопрос в другой теме https://stackoverflow.com/a/12846865/1685865 .Короче говоря, применение частичной функции заключается в исправлении некоторых аргументов данной многомерной функции для получения другой функции с меньшим количеством аргументов, в то время как каррирование заключается в превращении функции из 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

ради приготовления Карри.

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

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

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

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

Источник: Приготовление Карри в Википедии.

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

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

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

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

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

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

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

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

Если бы мне нужна была функция "addOne", я мог бы частично применить ее или изменить:

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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


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

(на Javascript)

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

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

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

и призовите его к действию субъект1 и фубар с

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

Отказ от ответственности

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

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

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

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

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

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

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

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

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

plus :: (Int, Int) -> Int

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

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

(uncurry (+)) (1,2)

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

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

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

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

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

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

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

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

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

Частичное Применение это когда ты звонить функция.

Применение это математический термин для вызова функции.

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top