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

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

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

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

Ответ 1

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

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

Ответ 2

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

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

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

Ответ 3

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

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

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

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

let double2 z = multiply 2 z

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

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

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

Ответ 4

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

Еще одна полезная страница (которую, я признаю, я еще не полностью прочитал), "Currying and Partial Application with Java Closures" .

Это похоже на то, что это очень запутанная пара терминов, заметьте.

Ответ 5

Я ответил на это в другом потоке fooobar.com/questions/17151/.... Короче говоря, приложение с частичной функцией - это фиксация некоторых аргументов заданной многопараметрической функции для получения другой функции с меньшим количеством аргументов, в то время как Currying - это превращение функции из N аргументов в унарную функцию, которая возвращает унарную функцию... [Пример Каррирование показано в конце этого сообщения.]

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

(Пример каррирования)

На практике можно было бы просто написать

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

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

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

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

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

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

Ответ 7

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

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

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

6 === partial(2, 3);

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

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

Ответ 8

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

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

Ответ 9

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

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

Ответ 10

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

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

Короче говоря, используйте частичное приложение для предварительной заливки некоторых значений, зная, что при следующем вызове метода он будет выполняться, оставив undefined все недопустимые аргументы; используйте 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

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

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

Ответ 11

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

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

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


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

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

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

отказ

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

Ответ 12

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

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

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

Ответ 13

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

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

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

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

plus :: (Int, Int) -> Int

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

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

(uncurry (+)) (1,2)

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

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

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

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

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

Ответ 14

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

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

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

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

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

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

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