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

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

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

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

16 ответов

Решение

Карринг - это преобразование одной функции из 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, accumalator, 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(accumaltor,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# 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 вычисляется, он использует свой аргумент вместе с частично примененным значением для создания результата.

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

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

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

Я ответил на это в другой теме /questions/46408994/chastichnoe-primenenie-i-zakryitiya/46409032#46409032. Короче говоря, частичное применение функции - это исправление некоторых аргументов данной функции с несколькими переменными, чтобы получить другую функцию с меньшим количеством аргументов, в то время как 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

ради карри.

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

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

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

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

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

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

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

отказ

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

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

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

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

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

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

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

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

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

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

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

Примеры

Частичное применение и каррирование

function bothPartialAndCurry(firstArgument) {
    return function(secondArgument) {
        return firstArgument + secondArgument;
    }
}

const partialAndCurry = bothPartialAndCurry(1);
const result = partialAndCurry(2);

Частичное применение

function partialOnly(firstArgument, secondArgument) {
    return function(thirdArgument, fourthArgument, fifthArgument) {
        return firstArgument + secondArgument + thirdArgument + fourthArgument + fifthArgument;
    }
}

const partial = partialOnly(1, 2);
const result = partialAndCurry(3, 4, 5);

Каррирование

function curryOnly(firstArgument) {
    return function(secondArgument) {
        return function(thirdArgument) {
            return function(fourthArgument ) {
                return function(fifthArgument) {
                    return firstArgument + secondArgument + thirdArgument + fourthArgument + fifthArgument;
                }
            }
        }
    }
}

const curryFirst = curryOnly(1);
const currySecond = curryFirst(2);
const curryThird = currySecond(3);
const curryFourth = curryThird(4);
const result = curryFourth(5);

// or...

const result = curryOnly(1)(2)(3)(4)(5);

Соглашения об именах

Я напишу это, когда у меня будет время, а это скоро.

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

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

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

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

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

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

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

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

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

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

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

карри

Википедия говорит

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

Пример

      const add = (a, b) => a + b

const addC = (a) => (b) => a + b // curried function. Where C means curried

Частичное применение

Статья Just Enough FP: частичное применение

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

Пример

      const add = (a) => (b) => a + b

const add3 = add(3) // add3 is a partially applied function

add3(5) // 8

Разница в том,

  1. curryingэто техника (паттерн)
  2. partial applicationэто функция с некоторыми предопределенными аргументами (например, add3из предыдущего примера)

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

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

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

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

plus :: (Int, Int) -> Int

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

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

(uncurry (+)) (1,2)

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

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

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

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

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

Другие вопросы по тегам