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


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

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

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

13 364

13 ответов:

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

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

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 и возвращающую другую функцию, эффективная привязка x к определенному значению. Эта функция также принимает аргумент int, который можно рассматривать как значение для привязки к y. After вызывая эту вторую функцию, x и y оба связаны, поэтому результат произведение x и y, как определено в теле двойника.

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

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

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

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

Я ответил на это в другом потоке https://stackoverflow.com/a/12846865/1685865 . Короче говоря, применение частичной функции заключается в фиксации некоторых аргументов данной многомерной функции для получения другой функции с меньшим количеством аргументов, в то время как карринг-это превращение функции из N аргументов в унарную функцию, которая возвращает унарную функцию...[Пример Карринга показан в конце этого поста.]

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

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

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

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

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

надеюсь, что это помогает!

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

карринг является функцией один аргумент, который принимает функцию f и возвращает новую функцию h:

curry(f) = h 

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

part(f, 2) = g

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

partial(f, a) = curry(f)(a)

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

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

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

источник: Wikipedia Currying.

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

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

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

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

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

plus :: (Int, Int) -> Int

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

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

(uncurry (+)) (1,2)

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

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

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

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

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

простой ответ

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

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


простые советы

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

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


простой e (C) (образец)

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

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

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