В чем преимущество карри?

154

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

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

fun add(x, y) = x + y

и будет называться

add(3, 5)

в то время как карри версия

fun add x y = x + y 
(* short for val add = fn x => fn y=> x + y *)

и будет называться

add 3 5

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

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

Злой ученый
источник
54
Само по себе каррирование по существу бесполезно, но использование всех функций по умолчанию делает карринг более приятным для использования. Трудно оценить это, пока вы на самом деле не используете функциональный язык некоторое время.
CA Макканн
4
Нечто упомянутое мимоходом delnan в комментарии к ответу JoelEtherton, но, как я думал, я бы прямо упомянул, это то, что (по крайней мере, в Haskell) вы можете частично применять не только функции, но и конструкторы типов - это может быть довольно удобно; об этом можно подумать.
Поль
Все привели примеры Хаскелла. Можно задаться вопросом, что карри полезно только в Хаскеле.
Manoj R
@ManojR Все не привели примеров в Haskell.
13
1
Вопрос вызвал довольно интересную дискуссию о Reddit .
Яннис

Ответы:

126

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

add x y = x + y

и что вы хотите добавить 2 к каждому члену списка. В Haskell вы бы сделали это:

map (add 2) [1, 2, 3] -- gives [3, 4, 5]
-- actually one could just do: map (2+) [1, 2, 3], but that may be Haskell specific

Здесь синтаксис легче, чем если бы вы должны были создать функцию add2

add2 y = add 2 y
map add2 [1, 2, 3]

или если вы должны были сделать анонимную лямбда-функцию:

map (\y -> 2 + y) [1, 2, 3]

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

lookup1 :: [(Key, Value)] -> Key -> Value -- or perhaps it should be Maybe Value
lookup2 :: Map Key Value -> Key -> Value

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

myFunc :: (Key -> Value) -> .....

В заключение: каррирование хорошо, потому что оно позволяет вам специализировать / частично применять функции с использованием облегченного синтаксиса, а затем передавать эти частично примененные функции функции более высокого порядка, например mapили filter. Функции высшего порядка (которые принимают функции в качестве параметров или выдают их в качестве результатов) являются основой функционального программирования, а функции каррирования и частично применяемые функции позволяют использовать функции более высокого порядка гораздо эффективнее и лаконичнее.

Борис
источник
31
Стоит отметить, что из-за этого порядок аргументов, используемый для функций в Haskell, часто зависит от вероятности частичного применения, что, в свою очередь, позволяет использовать описанные выше преимущества (ха, ха) в большинстве ситуаций. Таким образом, карри по умолчанию оказывается еще более полезным, чем это видно из конкретных примеров, подобных приведенным здесь.
CA McCann
Wat. «Один из списка пар ключ / значение и ключ к значению, а другой - от карты к ключам к значениям и ключ к значению»
Матин Улхак,
@MateenUlhaq Это продолжение предыдущего предложения, где я предполагаю, что мы хотим получить значение на основе ключа, и у нас есть два способа сделать это. Предложение перечисляет эти два способа. Во-первых, вам дают список пар ключ / значение и ключ, для которого мы хотим найти значение, а другим способом нам дают правильную карту, и снова ключ. Это может помочь посмотреть на код, следующий сразу за предложением.
Борис
53

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

map (add 1) [1..10]
map (\ x -> add 1 x) [1..10]

Если у вас отвратительный лямбда-синтаксис, это еще хуже. (Я смотрю на тебя, JavaScript, Scheme и Python.)

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

Более фундаментально, не всегда очевидно, какая версия функции является «канонической». Например, взять map. Тип mapможно записать двумя способами:

map :: (a -> b) -> [a] -> [b]
map :: (a -> b) -> ([a] -> [b])

Какой из них «правильный»? Это на самом деле сложно сказать. На практике большинство языков используют первый - карта берет функцию и список и возвращает список. Однако, по сути, то, что на самом деле делает map - это отображение нормальных функций в список функций - оно берет функцию и возвращает функцию. Если карта карри, вам не нужно отвечать на этот вопрос: она делает оба очень элегантным образом.

Это становится особенно важным, когда вы обобщаете mapна типы, отличные от list.

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

Конечно, языки ML-стиля не имеют понятия множественных аргументов в каррированном или нетуризованном виде. f(a, b, c)Синтаксис фактически соответствует прохождению в кортеже (a, b, c)в f, так до fсих пор принимает только аргумент. На самом деле это очень полезное различие, которое я хотел бы иметь в других языках, потому что это делает естественным писать что-то вроде:

map f [(1,2,3), (4,5,6), (7, 8, 9)]

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

Тихон Джелвис
источник
1
«Языки в стиле ML не имеют представления о множественных аргументах в карри или в некуррированной форме»: в этом отношении стиль Haskell ML?
Джорджио
1
@ Джорджио: Да.
Тихон Джелвис
1
Интересно. Я немного знаю Хаскель и сейчас изучаю SML, поэтому интересно увидеть различия и сходства между этими двумя языками.
Джорджио
Отличный ответ, и если вы все еще не уверены, просто подумайте о конвейерах Unix, которые похожи на лямбда-потоки
Шридхар Сарнобат
«Практический» ответ не имеет большого значения, потому что многословия обычно избегают частичным применением , а не каррированием. И я бы поспорил, что здесь синтаксис лямбда-абстракции (несмотря на объявление типа) более умен, чем (по крайней мере) в Scheme, так как для его правильного синтаксического анализа требуется больше встроенных специальных синтаксических правил, что приводит к расширению спецификации языка без какого-либо усиления. о семантических свойствах.
FrankHB
24

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

Код для выполнения этого будет проще, чем если вам нужно сначала собрать все параметры вместе.

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

PSR
источник
14

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

Алекс Р
источник
2
Хотя мотивация здесь теоретическая, я думаю, что простота почти всегда является практическим преимуществом. Отсутствие беспокойства о функциях с несколькими аргументами делает мою жизнь проще, когда я программирую, так же, как если бы я работал с семантикой.
Тихон Джелвис
2
@TikhonJelvis Однако, когда вы программируете, каррирование дает вам другие поводы для беспокойства, например, компилятор не улавливает тот факт, что вы передали слишком мало аргументов функции, или даже получает неверное сообщение об ошибке в этом случае; когда вы не используете карри, ошибка гораздо более очевидна.
Алекс Р
У меня никогда не было таких проблем: GHC, по крайней мере, очень хорош в этом отношении. Компилятор всегда улавливает проблему такого рода и также имеет хорошие сообщения об ошибках для этой ошибки.
Тихон Джелвис
1
Я не могу согласиться с тем, что сообщения об ошибках квалифицируются как хорошие. Работоспособны, да, но они еще не хороши. Кроме того, эта проблема обнаруживается только в том случае, если она приводит к ошибке типа, т. Е. Если вы позже попытаетесь использовать результат как функцию, отличную от функции (или если вы введете аннотацию, но у нее есть проблемы с читаемыми ошибками, у нее есть свои проблемы). ); Местоположение сообщения об ошибке отделено от фактического местоположения.
Алекс Р
14

(Я приведу примеры на Хаскеле.)

  1. При использовании функциональных языков очень удобно, что вы можете частично применить функцию. Как в Haskell's (== x)- это функция, которая возвращает, Trueесли ее аргумент равен заданному термину x:

    mem :: Eq a => a -> [a] -> Bool
    mem x lst = any (== x) lst
    

    без карри мы получили бы несколько менее читаемый код:

    mem x lst = any (\y -> y == x) lst
    
  2. Это связано с программированием Tacit (см. Также стиль Pointfree на вики Haskell). Этот стиль ориентирован не на значения, представленные переменными, а на составление функций и то, как информация протекает через цепочку функций. Мы можем преобразовать наш пример в форму, которая вообще не использует переменные:

    mem = any . (==)
    

    Здесь мы рассматриваем ==как функцию от aдо a -> Boolи anyкак функцию от a -> Boolдо [a] -> Bool. Просто составив их, мы получим результат. Это все благодаря карри.

  3. Обратное, без карри, также полезно в некоторых ситуациях. Например, скажем, мы хотим разделить список на две части - элементы, которые меньше 10 и остальные, и затем объединить эти два списка. Разделение списка осуществляется с помощью (здесь мы также используем карри ). Результат имеет тип . Вместо того, чтобы извлекать результат в его первую и вторую части и объединять их с помощью , мы можем сделать это напрямую, не торопясь, какpartition (< 10)<([Int],[Int])++++

    uncurry (++) . partition (< 10)
    

Действительно, (uncurry (++) . partition (< 10)) [4,12,11,1]оценивает [4,1,12,11].

Есть также важные теоретические преимущества:

  1. Карринг необходим для языков, которые не имеют типов данных и имеют только функции, такие как лямбда-исчисление . Хотя эти языки бесполезны для практического использования, они очень важны с теоретической точки зрения.
  2. Это связано с существенным свойством функциональных языков - функции объекта первого класса. Как мы уже видели, преобразование из (a, b) -> cв a -> (b -> c)означает, что результат последней функции имеет тип b -> c. Другими словами, результатом является функция.
  3. (Не) карринг тесно связан с декартовыми закрытыми категориями , что является категориальным способом просмотра типизированных лямбда-исчислений.
Петр Пудлак
источник
Для бита "гораздо менее читаемый код", не так ли mem x lst = any (\y -> y == x) lst? (С обратной косой чертой).
Стусмит
Да, спасибо за указание на это, я исправлю это.
Петр Пудлак
9

Карри не просто синтаксический сахар!

Рассмотрим типовые сигнатуры add1(uncurried) и add2(curry):

add1 : (int * int) -> int
add2 : int -> (int -> int)

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

add1это функция , которая принимает 2-кортеж intи intи возвращает int. add2это функция, которая принимает intи возвращает другую функцию, которая в свою очередь принимает intи возвращает int.

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

apply(f, b) = f b

Теперь мы можем видеть разницу между add1и add2более четко. add1вызывается с двумя кортежами:

apply(add1, (3, 5))

но вызываетсяadd2 с, int а затем его возвращаемое значение вызывается с другимint :

apply(apply(add2, 3), 5)

РЕДАКТИРОВАТЬ: Существенным преимуществом карри является то, что вы получаете частичную заявку бесплатно. Допустим, вам нужна функция типа int -> int(скажем, для mapсписка), которая добавила 5 к своему параметру. Вы могли бы написать addFiveToParam x = x+5, или вы могли бы сделать эквивалент с помощью встроенной лямбды, но вы также могли бы гораздо легче (особенно в случаях менее тривиальных, чем этот) писать add2 5!

Пламя Птариена
источник
3
Я понимаю, что для моего примера есть большая разница за кулисами, но в результате получается простое синтаксическое изменение.
Безумный Ученый
5
Карри не очень глубокая концепция. Речь идет об упрощении базовой модели (см. Лямбда-исчисление) или языков, которые в любом случае имеют кортежи, на самом деле речь идет о синтаксическом удобстве частичного применения. Не стоит недооценивать важность синтаксического удобства.
Peaker
9

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

fun add x y = x + y

на самом деле синтаксический сахар для

fun add x = fn y => x + y

То есть (add x) возвращает функцию, которая принимает аргумент y и добавляет x к y.

fun addTuple (x, y) = x + y

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

Если вы хотите добавить 2 ко всем номерам в списке:

(* add 2 to all numbers using the uncurried function *)
map (fn x => addTuple (x, 2)) [1,2,3]
(* using the curried function *)
map (add 2) [1,2,3]

Результат будет [3,4,5].

Если вы хотите суммировать каждый кортеж в списке, с другой стороны, функция addTuple подходит идеально.

(* Sum each tuple using the uncurried function *)
map addTuple [(10,2), (10,3), (10,4)]    
(* sum each tuple using curried function *)
map (fn (a,b) => add a b) [(10,2), (10,3), (10,4)]

Результат будет [12,13,14].

Каррированные функции хороши там, где полезно частичное приложение - например, map, fold, app, filter. Рассмотрим эту функцию, которая возвращает наибольшее положительное число в представленном списке или 0, если положительных чисел нет:

- val highestPositive = foldr Int.max 0;   
val highestPositive = fn : int list -> int 
gnud
источник
1
Я понял, что функция карри имеет сигнатуру другого типа, и что на самом деле это функция, которая возвращает другую функцию. Я пропустил частичную часть заявки, хотя.
Безумный ученый
9

Еще одна вещь, которую я еще не упомянул, - это то, что каррирование позволяет (ограниченной) абстрагироваться над арностью.

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

(.) :: (b -> c) -> (a -> b) -> a -> c
either :: (a -> c) -> (b -> c) -> Either a b -> c
flip :: (a -> b -> c) -> b -> a -> c
on :: (b -> b -> c) -> (a -> b) -> a -> a -> c

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

Джефф Риди
источник
6

Мое ограниченное понимание таково:

1) Применение частичной функции

Частичное применение функции - это процесс возврата функции, которая принимает меньшее количество аргументов. Если вы предоставите 2 из 3 аргументов, он вернет функцию, которая принимает аргумент 3-2 = 1. Если вы предоставите 1 из 3 аргументов, он вернет функцию, которая принимает 3-1 = 2 аргумента. Если бы вы хотели, вы могли бы даже частично применить 3 из 3 аргументов, и это вернуло бы функцию, которая не принимает аргументов.

Так дана следующая функция:

f(x,y,z) = x + y + z;

При связывании 1 с x и частичном применении этого к вышеуказанной функции f(x,y,z)вы получите:

f(1,y,z) = f'(y,z);

Где: f'(y,z) = 1 + y + z;

Теперь, если бы вы связали y с 2 и z с 3 и применили частично, f'(y,z)вы получите:

f'(2,3) = f''();

Где f''() = 1 + 2 + 3:;

Теперь в любой момент, вы можете оценить f, f'или f''. Так что я могу сделать:

print(f''()) // and it would return 6;

или же

print(f'(1,1)) // and it would return 3;

2) Карри

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

Итак, с учетом той же функции:

f(x,y,z) = x + y + z;

Если вы его каррируете, вы получите цепочку из 3 функций:

f'(x) -> f''(y) -> f'''(z)

Где:

f'(x) = x + f''(y);

f''(y) = y + f'''(z);

f'''(z) = z;

Теперь, если вы звоните f'(x)с x = 1:

f'(1) = 1 + f''(y);

Вам возвращается новая функция:

g(y) = 1 + f''(y);

Если вы звоните g(y)с y = 2:

g(2) = 1 + 2 + f'''(z);

Вам возвращается новая функция:

h(z) = 1 + 2 + f'''(z);

Наконец, если вы звоните h(z)с z = 3:

h(3) = 1 + 2 + 3;

Вы вернулись 6.

3) Закрытие

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

Опять же, учитывая ту же функцию:

f(x,y,z) = x + y + z;

Вместо этого вы можете написать закрытие:

f(x) = x + f'(y, z);

Где:

f'(y,z) = x + y + z;

f'закрыт на x. Это означает, что f'можно прочитать значение х внутри f.

Так что, если вам нужно позвонить fс x = 1:

f(1) = 1 + f'(y, z);

Вы получите закрытие:

closureOfF(y, z) =
                   var x = 1;
                   f'(y, z);

Теперь, если вы звонили closureOfFс y = 2и z = 3:

closureOfF(2, 3) = 
                   var x = 1;
                   x + 2 + 3;

Который вернется 6

Заключение

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

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

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

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

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

раскрытие

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

Дидье А.
источник
1
Таким образом, ответ: карри не имеет преимущества?
ceving
1
@ceving Насколько я знаю, это правильно. На практике карри и частичное применение принесут вам те же преимущества. Выбор, который нужно реализовать на языке, сделан из соображений реализации, один может быть легче реализовать, чем другой, учитывая определенный язык.
Дидье А.
5

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

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

Вместо того (lambda (x) (+ 3 x)), что дает нам функцию, которая добавляет 3 к своему аргументу, вы можете написать что-то вроде (op + 3), и поэтому добавить 3 к каждому элементу некоторого списка было бы (mapcar (op + 3) some-list)лучше, чем (mapcar (lambda (x) (+ 3 x)) some-list). Этот opмакрос сделает вас функцией, которая принимает некоторые аргументы x y z ...и вызывает их (+ a x y z ...).

Во многих чисто функциональных языках частичное применение встроено в синтаксис, так что нет opоператора. Чтобы запустить частичное приложение, вы просто вызываете функцию с меньшим количеством аргументов, чем требуется. Вместо того, чтобы выдавать "insufficient number of arguments"ошибку, результат является функцией оставшихся аргументов.

Kaz
источник
«Карринг ... позволяет создать новую функцию ... фиксируя некоторые параметры» - нет, функция типа a -> b -> cне имеет параметр s ( во множественном числе), он имеет только один параметр, c. При вызове возвращает функцию типа a -> b.
Макс
4

Для функции

fun add(x, y) = x + y

Это имеет форму f': 'a * 'b -> 'c

Чтобы оценить один будет делать

add(3, 5)
val it = 8 : int

Для карри функции

fun add x y = x + y

Чтобы оценить один будет делать

add 3
val it = fn : int -> int

Где это частичное вычисление, в частности (3 + y), которое затем можно завершить вычислением

it 5
val it = 8 : int

добавить во втором случае имеет вид f: 'a -> 'b -> 'c

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

Зачем это нужно?

Скажем, xRHS - это не просто обычное целое, а сложное вычисление, которое занимает некоторое время, для увеличения, ради двух секунд.

x = twoSecondsComputation(z)

Так что функция теперь выглядит так

fun add (z:int) (y:int) : int =
    let
        val x = twoSecondsComputation(z)
    in
        x + y
    end;

Типа add : int * int -> int

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

val result1 = map (fn x => add (20, x)) [3, 5, 7];

Для вышеизложенного результат twoSecondsComputationоценивается каждый раз. Это означает, что это вычисление занимает 6 секунд.

Используя комбинацию постановки и карри, можно избежать этого.

fun add (z:int) : int -> int =
    let
        val x = twoSecondsComputation(z)
    in
        (fn y => x + y)
    end;

Карри add : int -> int -> int

Теперь можно сделать,

val add' = add 20;
val result2 = map add' [3, 5, 7, 11, 13];

В twoSecondsComputationтолько нужно оценивать один раз. Чтобы увеличить масштаб, замените две секунды на 15 минут или любой час, а затем сопоставьте 100 чисел.

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

phwd
источник
3

Карри позволяет гибкую функцию композиции.

Я составил функцию «карри». В этом контексте мне все равно, какой регистратор я получу или откуда он взялся. Мне все равно, что это за действие или откуда оно берется. Все, что меня волнует, - это обработка моего ввода.

var builder = curry(function(input, logger, action) {
     logger.log("Starting action");
     try {
         action(input);
         logger.log("Success!");
     }
     catch (err) {
         logger.logerror("Boo we failed..", err);
     }
});
var x = "My input.";
goGatherArgs(builder)(x); // Supplies action first, then logger somewhere.

Переменная построителя - это функция, которая возвращает функцию, которая возвращает функцию, которая принимает мой ввод и выполняет мою работу. Это простой полезный пример, а не видимый объект.

mortalapeman
источник
2

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

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

Например, при использовании функций, которые принимают функции в качестве аргументов, вы часто будете сталкиваться с ситуациями, когда вам нужны такие функции, как «добавить 3 к вводу» или «сравнить ввод с переменной v». С помощью карри эти функции легко записываются: add 3и (== v). Без каррирования вы должны использовать лямбда-выражения: x => add 3 xи x => x == v. Лямбда-выражения в два раза длиннее и содержат небольшое количество занятой работы, связанной с выбором имени, кроме того, xесли уже есть xобласть действия.

Дополнительным преимуществом языков, основанных на карри, является то, что при написании универсального кода для функций вы не получите сотни вариантов, основанных на количестве параметров. Например, в C # методу «карри» потребуются варианты для Func <R>, Func <A, R>, Func <A1, A2, R>, Func <A1, A2, A3, R> и т. Д. навсегда. В Haskell эквивалент Func <A1, A2, R> больше похож на Func <Tuple <A1, A2>, R> или Func <A1, Func <A2, R >> (и Func <R> больше похоже на Func <Unit, R>), поэтому все варианты соответствуют одному случаю Func <A, R>.

Крейг Гидни
источник
2

Основные рассуждения, которые я могу придумать (и я ни в коем случае не являюсь экспертом в этом вопросе), начинают проявлять свои преимущества по мере того, как функции переходят от тривиальных к нетривиальным. Во всех тривиальных случаях с большинством понятий такого рода вы не найдете никакой реальной выгоды. Однако большинство функциональных языков интенсивно используют стек в операциях обработки. Рассмотрим PostScript или Lisp в качестве примеров этого. Используя карри, функции могут быть сложены более эффективно, и это преимущество становится очевидным, поскольку операции становятся все менее и менее тривиальными. Таким образом, команда и аргументы могут быть выброшены в стек по порядку и вытолкнуты по мере необходимости, чтобы они выполнялись в правильном порядке.

Peter Mortensen
источник
1
Каким образом создание более эффективных фреймов стека делает вещи более эффективными?
Мейсон Уилер
1
@MasonWheeler: Я бы не знал, так как я сказал, что я не специалист по функциональным языкам или конкретно по карри. Я назвал это сообщество вики именно из-за этого.
Джоэл Этертон
4
@MasonWheeler Ваша точка зрения по поводу формулировки этого ответа, но позвольте мне сказать, что количество фактически созданных кадров стека во многом зависит от реализации. Например, в G-машине без тегов без тегов (STG; способ, которым GHC реализует Haskell) задерживает фактическую оценку до тех пор, пока она не соберет все (или, по крайней мере, столько, сколько ей потребуется) аргументов. Я не могу вспомнить, сделано ли это для всех функций или только для конструкторов, но я думаю, что это должно быть возможно для большинства функций. (Опять же, концепция «стековых фреймов» на самом деле не относится к STG.)
1

Карринг в решающей степени зависит от способности вернуть функцию.

Рассмотрим этот (надуманный) псевдокод.

var f = (m, x, b) => ... вернуть что-то ...

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

var g = f (0, 1); // это возвращает функцию, привязанную к 0 и 1 (m и x), которая принимает еще один аргумент (b).

var y = g (42); // вызвать g с отсутствующим третьим аргументом, используя 0 и 1 для m и x

То, что вы можете частично применить аргументы и получить обратно многократно используемую функцию (привязанную к тем аргументам, которые вы предоставили), весьма полезно (и СУХОЙ).

Рик О'Ши
источник