Чим відрізняється каррі від часткового застосування?


438

Я досить часто бачу в Інтернеті різні скарги на те, що інші народи прикладів каррінгу не є, але насправді є лише частковим застосуванням.

Я не знайшов гідного пояснення, що таке часткове застосування або чим воно відрізняється від каррінгу. Здається, загальна плутанина, в еквівалентних прикладах описується як десерт, а в інших часткове застосування.

Чи міг би хтось надати мені визначення як термінів, так і деталей того, як вони відрізняються?

Відповіді:


256

Currying - це перетворення однієї функції з 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

40
Ви говорите, що часткове застосування - це коли ви виконайте функцію і використовуєте деякі, але не всі отримані функції?
SpoonMeiser

9
більш-менш, так. Якщо ви надасте лише підмножину аргументів, ви отримаєте функцію, яка приймає решту аргументів
Марк Cidade

1
Чи вважатиме зміна функції f (a, b, c, d) на g (a, b) частковою? Або це лише при застосуванні до кривих функцій? Вибачте, що болю, але я тугу за чіткою відповіддю.
SpoonMeiser

2
@Mark: Я думаю, це лише одна з тих концепцій, яка виявляє педант в мені - але звернення до авторитетних джерел мало задовольняє, оскільки вони, схоже, вказують один на одного. Вікіпедія навряд чи те, що я вважаю авторитетним джерелом, але я розумію, що важко знайти багато іншого. Досить сказати, що я думаю, що ми обидва знаємо те, про що ми говоримо, і силу цього, незалежно від того, чи можемо ми погодитися (чи не погодитись) на деталі просторіччя! :) Дякую Марку!
Джейсон Бантінг

5
@JasonBunting, Що стосується вашого першого коментаря, те, про що ви говорили, ухиляється . Додавання корисних корінців - це функція мульти-аргументів як вхід, а повернення ланцюжка функцій 1-arg як вихід. Де-крирінг - це прийняття ланцюга функцій 1-arg як вхід, а повернення функції multi-arg як вихід. Як докладно на stackoverflow.com/a/23438430/632951
Pacerier

165

Найпростіший спосіб побачити, як вони відрізняються - розглянути реальний приклад . Припустимо, що у нас є функція, 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 (або більше) входів.

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


24
Часткове додаток перетворює функцію з n-aryв (x - n)-ary, перетворюючись на n-aryв n * 1-ary. Частково застосована функція має зменшений обсяг (застосування), тобто Add7менш виразна, ніж Add. Зворотна функція з іншого боку така ж виразна, як і оригінальна функція.
боб

4
Я вважаю, що більш відмітною рисою є те, коли ми curry f (x, y, z) => R, ми отримуємо f (x), який повертає g (y) => h (z) => R, кожен з яких споживає один аргумент; але коли ми частково застосовуємо f (x, y, z) як f (x), отримуємо g (y, z) => R, тобто з двома аргументами. Якщо б не ця ознака, ми могли б сказати, що currying - це як часткове застосування до 0 аргументів, таким чином, залишаючи всі аргументи незв'язаними; проте насправді f (), частково застосований до 0 аргументів, - це функція, що споживає 3 аргументи одночасно, на відміну від curried f ().
Максим Гумеров

2
Ще раз правильна відповідь - це не перша чи найбільш голосована: Просте пояснення підпису каррі проти часткового в кінці цієї відповіді справді найпростіший спосіб вирішити питання.
fnl

2
Що означає коментар f2(7)(5) is just a syntactic shortcut? (Я дуже мало знаю.) Ще не f2містить / "знаю про" 7?
Zach Mierzejewski

@Pacerier, чи є curryдесь реалізація (не думайте, що це functools)
alancalvitti

51

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

Добуток означає розбиття функції з багатьма аргументами на низку функцій, кожна з яких бере один аргумент і в кінцевому підсумку дає такий же результат, що і вихідна функція. Крірінг - це, мабуть, найскладніша тема для розробників, які є новими для функціонального програмування, особливо тому, що його часто плутають з частковим застосуванням. У цьому прикладі ви можете побачити обох на роботі:

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

Відразу вам слід побачити поведінку, яка відрізняється від більшості імперативних мов. Другий вираз створює нову функцію, яку називають подвійною, передаючи один аргумент функції, яка займає два. Результат - це функція, яка приймає один int-аргумент і дає такий же вихід, як якщо б ви покликали помножити на x, що дорівнює 2, і y, що дорівнює цьому аргументу. Що стосується поведінки, це те саме, що і цей код:

let double2 z = multiply 2 z

Часто люди помилково говорять, що розмножуються викривлені, щоб утворити подвійне. Але це лише дещо вірно. Функція множення є curried, але це відбувається, коли вона визначена, оскільки функції в F # за замовчуванням. Коли створена подвійна функція, точніше сказати, що функція множення частково застосовується.

Функція множення - це дійсно серія з двох функцій. Перша функція приймає один int аргумент і повертає іншу функцію, ефективно прив’язуючи x до певного значення. Ця функція також приймає аргумент int, який можна вважати значенням, яке пов'язується з y. Після виклику цієї другої функції x і y є обома зв'язаними, тому результатом є добуток x і y, як визначено в тілі подвійного.

Щоб створити подвійне, перша функція в ланцюзі функцій множення оцінюється, щоб частково застосувати множення. Отриманій функції надається ім'я подвійне. Коли подвійне оцінюється, він використовує свій аргумент разом з частково застосованим значенням для створення результату.


33

Цікаве запитання. Після невеликого пошуку "Програма з частковою функцією не криється" дала найкраще пояснення, яке я знайшов. Я не можу сказати, що практична різниця для мене особливо очевидна, але тоді я не фахівець з ПП ...

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

Схоже, це дуже заплутана пара термінів, пам’ятайте.


5
Перше посилання є точкою про відмінності. Ось ще один, який мені здався
Jason Bunting

5
Каррінг - це пов'язане з кортежами (перетворення функції, яка бере аргумент кортежу, в аргумент, який бере n окремих аргументів, і навпаки). Часткове застосування - це можливість застосувати функцію до деяких аргументів, даючи нову функцію для решти аргументів. Легко запам'ятати, якщо ви просто думаєте, що currying == робити з кортежами.
Дон Стюарт

9
@ Jon посилання, які ви опублікували, є інформативними, але краще буде розширити свою відповідь та додати трохи більше інформації тут.
Захер Ахмед


11
Не можу повірити, що ви отримали 20 грошей за пару посилань та визнання, що ви насправді не знаєте різниці між каррі та частковим застосуванням. Добре зіграно, сер.
AlienWebguy

16

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

заради Крірінг.


1
Ви б сказали, що тоді каррінг - це конкретний випадок часткового застосування?
SpoonMeiser

1
@SpoonMeiser, Ні, currying - це не конкретний випадок часткового застосування: Часткове застосування 2-вхідної функції не те саме, що currying функції. Дивіться stackoverflow.com/a/23438430/632951 .
Pacerier

10

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

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

Джерело: Wikipedia Currying .


8

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

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

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

6 === partial(2, 3);

Часткове застосування призводить до функції меншої артії; у наведеному вище прикладі fмає атрибут 3, тоді partialяк має лише атрибут 2. Більш важливо, що частково застосована функція повертає результат одразу після виклику , а не іншої функції вниз по ланцюгу currying. Тож якщо ви бачите щось подібне partial(2)(3), це не часткове застосування в дійсності.

Подальше читання:


"частково застосована функція повертає результат відразу після виклику" - це неправильно, чи не так? коли я частково застосовую функцію, це вираз повертає функцію, а не "результат". Гаразд, ви, мабуть, мали на увазі, що ця остання функція, коли викликається з аргументами, що залишилися, повертає результат, на відміну від копання на один крок вниз в каррінг. Але ніхто насправді не каже, що вам потрібно вказати всі аргументи, що залишилися: ви можете частково застосувати результат часткового застосування, і це знову буде функцією, а не "результатом"
Максим Гумеров

6

Проста відповідь

Каррі: дозволяє викликати функцію, розділяючи її на кілька викликів, надаючи один аргумент за виклик.

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


Прості підказки

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

Реальну різницю можна побачити, коли функція має більше 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');

Відмова від відповідальності

Я пропустив усі академічні / математичні пояснення. Тому що я цього не знаю. Можливо, це допомогло 🙃


4

У мене це питання було багато під час навчання, і з тих пір його задавали багато разів. Найпростіший спосіб я описати різницю - це те, що обидва однакові :) Дозвольте пояснити ... очевидно є відмінності.

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

Коротше кажучи, використовуйте часткове додаток для попереднього заповнення деяких значень, знаючи, що наступного разу, коли ви зателефонуєте методу, він виконає, залишаючи невизначеними всі незабезпечені аргументи; використовуйте 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 дозволять вам передати сукупність (загальну кількість аргументів у підсумковій оцінці) до часткової реалізації програми, яка може зв'язати два мої описи в заплутаному безладі ... але в цей момент ці дві методи багато в чому взаємозамінні.


3

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

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


3

Я міг би помилитися тут, оскільки не маю сильної передумови в теоретичній математиці чи функціональному програмуванні, але з мого короткого переходу в FP, здається, що currying має тенденцію перетворювати функцію 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

Це, очевидно, надуманий приклад, але зауважте, що часткове застосування функції, яка приймає будь-яку кількість аргументів, дозволяє виконувати функцію, але з деякими попередніми даними. Функція currying аналогічна, але дозволяє виконувати функцію N-параметрів частинами до тих пір, але лише до тих пір, поки не враховуються всі N параметрів.

Знову ж таки, це моя думка з речей, які я прочитав. Якщо хтось не погоджується, я буду вдячний за коментар, чому, а не негайне голосування. Крім того, якщо CoffeeScript важко прочитати, будь ласка, відвідайте coffeescript.org, натисніть "спробувати coffeescript" і вставте мій код, щоб побачити складену версію, що може (сподіваємось) мати більше сенсу. Дякую!


2

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

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

Замість того, щоб визначати, що таке кожен, простіше виділити лише їх відмінності - межу.

Каррінг - це коли ви визначаєте функцію.

Часткова заявка - це коли ви телефонуєте функцію.

Застосування є математичним мовою для виклику функції.

Часткове застосування вимагає викликати функцію curried та отримати функцію як тип повернення.


1

Тут є й інші чудові відповіді, але я вважаю, що цей приклад (наскільки я розумію) на 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 );
}

Наступне дало мені ключове розуміння: "Отже, currying дає вам функцію з одним аргументом для створення функцій, де часткове застосування створює функцію обгортки, яка жорстко кодує один або кілька аргументів."
Roland

0

Пишучи це, я переплутав curry і uncurrying. Вони є зворотними перетвореннями на функції. Це насправді не має значення, як ви називаєте це, доки ви отримаєте те, що представляють трансформація та її зворотна.

Спростування не визначено дуже чітко (точніше, є "суперечливі" визначення, які все захоплюють дух ідеї). В основному це означає перетворення функції, яка бере кілька аргументів, у функцію, яка приймає один аргумент. Наприклад,

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

Тепер, як ви перетворите це на функцію, яка приймає єдиний аргумент? Ви обманюєте, звичайно!

plus :: (Int, Int) -> Int

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

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

(uncurry (+)) (1,2)

Отже, що таке часткове застосування функції? Це інший спосіб перетворити функцію з двох аргументів у функцію одним аргументом. Хоча це працює інакше. Знову візьмемо (+) як приклад. Як ми можемо перетворити його на функцію, яка сприймає єдиний Int як аргумент? Ми обманюємо!

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

Це функція, яка додає нуль до будь-якого Int.

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

додає 1 до будь-якого Int. І т.д. В кожному з цих випадків (+) "частково застосовується".

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.