Я только что узнал о карри, и хотя я думаю, что понимаю концепцию, я не вижу большого преимущества в ее использовании.
В качестве тривиального примера я использую функцию, которая добавляет два значения (написано в 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
Мне кажется, это просто синтаксический сахар, который удаляет один набор скобок из определения и вызова функции. Я видел карри в качестве одной из важных функций функциональных языков, и в настоящий момент меня это не устраивает. Концепция создания цепочки функций, которые используют каждый отдельный параметр вместо функции, которая принимает кортеж, кажется довольно сложной для использования для простого изменения синтаксиса.
Является ли немного более простой синтаксис единственной мотивацией для карри или я упускаю некоторые другие преимущества, которые не очевидны в моем очень простом примере? Карри - это просто синтаксический сахар?
источник
Ответы:
С каррированными функциями вы получаете более простое повторное использование более абстрактных функций, поскольку вы получаете специализацию. Допустим, у вас есть функция добавления
и что вы хотите добавить 2 к каждому члену списка. В Haskell вы бы сделали это:
Здесь синтаксис легче, чем если бы вы должны были создать функцию
add2
или если вы должны были сделать анонимную лямбда-функцию:
Это также позволяет абстрагироваться от различных реализаций. Допустим, у вас было две функции поиска. Одна из списка пар ключ / значение и ключ к значению, а другая - из карты из ключей к значениям и ключ к значению, например:
Затем вы могли бы создать функцию, которая принимала бы функцию поиска из Key to Value. Вы можете передать его любой из указанных выше функций поиска, частично примененных либо к списку, либо к карте соответственно:
В заключение: каррирование хорошо, потому что оно позволяет вам специализировать / частично применять функции с использованием облегченного синтаксиса, а затем передавать эти частично примененные функции функции более высокого порядка, например
map
илиfilter
. Функции высшего порядка (которые принимают функции в качестве параметров или выдают их в качестве результатов) являются основой функционального программирования, а функции каррирования и частично применяемые функции позволяют использовать функции более высокого порядка гораздо эффективнее и лаконичнее.источник
Практический ответ заключается в том, что каррирование значительно упрощает создание анонимных функций. Даже с минимальным лямбда-синтаксисом это что-то вроде победы; Для сравнения:
Если у вас отвратительный лямбда-синтаксис, это еще хуже. (Я смотрю на тебя, JavaScript, Scheme и Python.)
Это становится все более полезным, когда вы используете все больше и больше функций высшего порядка. В то время как я использую больше функций высшего порядка в Haskell , чем в других языках, я обнаружил , что я на самом деле использовать синтаксис лямбда меньше , потому что что - то вроде двух третей времени, лямбда будет просто частично прикладной функции. (И большую часть времени я извлекаю его в именованную функцию.)
Более фундаментально, не всегда очевидно, какая версия функции является «канонической». Например, взять
map
. Типmap
можно записать двумя способами:Какой из них «правильный»? Это на самом деле сложно сказать. На практике большинство языков используют первый - карта берет функцию и список и возвращает список. Однако, по сути, то, что на самом деле делает map - это отображение нормальных функций в список функций - оно берет функцию и возвращает функцию. Если карта карри, вам не нужно отвечать на этот вопрос: она делает оба очень элегантным образом.
Это становится особенно важным, когда вы обобщаете
map
на типы, отличные от list.Кроме того, карри на самом деле не очень сложно. На самом деле это немного упрощает модель, используемую большинством языков: вам не нужно понимать функции нескольких аргументов, встроенных в ваш язык. Это также более точно отражает лежащее в основе лямбда-исчисление.
Конечно, языки ML-стиля не имеют понятия множественных аргументов в каррированном или нетуризованном виде.
f(a, b, c)
Синтаксис фактически соответствует прохождению в кортеже(a, b, c)
вf
, так доf
сих пор принимает только аргумент. На самом деле это очень полезное различие, которое я хотел бы иметь в других языках, потому что это делает естественным писать что-то вроде:Вы не могли бы легко сделать это с языками, которые имеют идею множественных аргументов, запеченных прямо в!
источник
Карринг может быть полезен, если у вас есть функция, которую вы передаете как объект первого класса, и вы не получаете все параметры, необходимые для ее оценки, в одном месте кода. Вы можете просто применить один или несколько параметров, когда получите их, и передать результат другому фрагменту кода, который имеет больше параметров, и завершить его оценку там.
Код для выполнения этого будет проще, чем если вам нужно сначала собрать все параметры вместе.
Кроме того, существует возможность повторного использования кода, поскольку функции, принимающие один параметр (другую функцию с каррированием), не обязательно должны точно соответствовать всем параметрам.
источник
Основная мотивация (по крайней мере на начальном этапе) карри была не практической, а теоретической. В частности, каррирование позволяет эффективно получать функции с несколькими аргументами без фактического определения семантики для них или определения семантики для продуктов. Это приводит к более простому языку с такой же выразительностью, как к другому, более сложному языку, и поэтому это желательно.
источник
(Я приведу примеры на Хаскеле.)
При использовании функциональных языков очень удобно, что вы можете частично применить функцию. Как в Haskell's
(== x)
- это функция, которая возвращает,True
если ее аргумент равен заданному терминуx
:без карри мы получили бы несколько менее читаемый код:
Это связано с программированием Tacit (см. Также стиль Pointfree на вики Haskell). Этот стиль ориентирован не на значения, представленные переменными, а на составление функций и то, как информация протекает через цепочку функций. Мы можем преобразовать наш пример в форму, которая вообще не использует переменные:
Здесь мы рассматриваем
==
как функцию отa
доa -> Bool
иany
как функцию отa -> Bool
до[a] -> Bool
. Просто составив их, мы получим результат. Это все благодаря карри.Обратное, без карри, также полезно в некоторых ситуациях. Например, скажем, мы хотим разделить список на две части - элементы, которые меньше 10 и остальные, и затем объединить эти два списка. Разделение списка осуществляется с помощью (здесь мы также используем карри ). Результат имеет тип . Вместо того, чтобы извлекать результат в его первую и вторую части и объединять их с помощью , мы можем сделать это напрямую, не торопясь, как
partition
(< 10)
<
([Int],[Int])
++
++
Действительно,
(uncurry (++) . partition (< 10)) [4,12,11,1]
оценивает[4,1,12,11]
.Есть также важные теоретические преимущества:
(a, b) -> c
вa -> (b -> c)
означает, что результат последней функции имеет типb -> c
. Другими словами, результатом является функция.источник
mem x lst = any (\y -> y == x) lst
? (С обратной косой чертой).Карри не просто синтаксический сахар!
Рассмотрим типовые сигнатуры
add1
(uncurried) иadd2
(curry):(В обоих случаях скобки в сигнатуре типа являются необязательными, но я включил их для ясности.)
add1
это функция , которая принимает 2-кортежint
иint
и возвращаетint
.add2
это функция, которая принимаетint
и возвращает другую функцию, которая в свою очередь принимаетint
и возвращаетint
.Существенное различие между ними становится более заметным, когда мы явно указываем приложение функции. Давайте определим функцию (не карри), которая применяет свой первый аргумент ко второму аргументу:
Теперь мы можем видеть разницу между
add1
иadd2
более четко.add1
вызывается с двумя кортежами:но вызывается
add2
с,int
а затем его возвращаемое значение вызывается с другимint
:РЕДАКТИРОВАТЬ: Существенным преимуществом карри является то, что вы получаете частичную заявку бесплатно. Допустим, вам нужна функция типа
int -> int
(скажем, дляmap
списка), которая добавила 5 к своему параметру. Вы могли бы написатьaddFiveToParam x = x+5
, или вы могли бы сделать эквивалент с помощью встроенной лямбды, но вы также могли бы гораздо легче (особенно в случаях менее тривиальных, чем этот) писатьadd2 5
!источник
Карри - это просто синтаксический сахар, но я думаю, вы немного не понимаете, что делает сахар. Принимая ваш пример,
на самом деле синтаксический сахар для
То есть (add x) возвращает функцию, которая принимает аргумент y и добавляет x к y.
Это функция, которая принимает кортеж и добавляет его элементы. Эти две функции на самом деле довольно разные; они принимают разные аргументы.
Если вы хотите добавить 2 ко всем номерам в списке:
Результат будет
[3,4,5]
.Если вы хотите суммировать каждый кортеж в списке, с другой стороны, функция addTuple подходит идеально.
Результат будет
[12,13,14]
.Каррированные функции хороши там, где полезно частичное приложение - например, map, fold, app, filter. Рассмотрим эту функцию, которая возвращает наибольшее положительное число в представленном списке или 0, если положительных чисел нет:
источник
Еще одна вещь, которую я еще не упомянул, - это то, что каррирование позволяет (ограниченной) абстрагироваться над арностью.
Рассмотрим эти функции, которые являются частью библиотеки Haskell
В каждом случае переменная типа
c
может быть типом функции, так что эти функции работают с некоторым префиксом в списке параметров их аргумента. Без каррирования вам понадобится либо специальная языковая функция для абстрагирования над арностью функции, либо много разных версий этих функций, предназначенных для разных арностей.источник
Мое ограниченное понимание таково:
1) Применение частичной функции
Частичное применение функции - это процесс возврата функции, которая принимает меньшее количество аргументов. Если вы предоставите 2 из 3 аргументов, он вернет функцию, которая принимает аргумент 3-2 = 1. Если вы предоставите 1 из 3 аргументов, он вернет функцию, которая принимает 3-1 = 2 аргумента. Если бы вы хотели, вы могли бы даже частично применить 3 из 3 аргументов, и это вернуло бы функцию, которая не принимает аргументов.
Так дана следующая функция:
При связывании 1 с x и частичном применении этого к вышеуказанной функции
f(x,y,z)
вы получите:Где:
f'(y,z) = 1 + y + z;
Теперь, если бы вы связали y с 2 и z с 3 и применили частично,
f'(y,z)
вы получите:Где
f''() = 1 + 2 + 3
:;Теперь в любой момент, вы можете оценить
f
,f'
илиf''
. Так что я могу сделать:или же
2) Карри
Карри с другой стороны - это процесс разбиения функции на вложенную цепочку функций с одним аргументом. Вы никогда не можете предоставить более 1 аргумента, это один или ноль.
Итак, с учетом той же функции:
Если вы его каррируете, вы получите цепочку из 3 функций:
Где:
Теперь, если вы звоните
f'(x)
сx = 1
:Вам возвращается новая функция:
Если вы звоните
g(y)
сy = 2
:Вам возвращается новая функция:
Наконец, если вы звоните
h(z)
сz = 3
:Вы вернулись
6
.3) Закрытие
Наконец, Closure - это процесс сбора функции и данных в единый блок. Закрытие функции может принимать от 0 до бесконечного числа аргументов, но оно также знает о данных, которые ему не передаются.
Опять же, учитывая ту же функцию:
Вместо этого вы можете написать закрытие:
Где:
f'
закрыт наx
. Это означает, чтоf'
можно прочитать значение х внутриf
.Так что, если вам нужно позвонить
f
сx = 1
:Вы получите закрытие:
Теперь, если вы звонили
closureOfF
сy = 2
иz = 3
:Который вернется
6
Заключение
Карринг, частичное применение и замыкания несколько похожи в том, что они разбивают функцию на несколько частей.
Карринг разлагает функцию нескольких аргументов на вложенные функции отдельных аргументов, которые возвращают функции отдельных аргументов. Нет смысла каррировать функцию с одним или несколькими аргументами, так как это не имеет смысла.
Частичное приложение разлагает функцию нескольких аргументов на функцию меньших аргументов, чьи пропущенные аргументы были заменены предоставленным значением.
Закрытие разлагает функцию на функцию и набор данных, где переменные внутри функции, которые не были переданы, могут заглянуть внутрь набора данных, чтобы найти значение, к которому необходимо привязаться, когда его попросят оценить.
Что смущает во всем этом, так это то, что они могут быть использованы для реализации подмножества других. Таким образом, по сути, все они немного детали реализации. Все они обеспечивают одинаковое значение в том смысле, что вам не нужно собирать все значения заранее, и в этом вы можете повторно использовать часть функции, поскольку вы разложили ее на дискретные блоки.
раскрытие
Я ни в коем случае не эксперт в этой теме, я только недавно начал изучать их, и поэтому я даю свое текущее понимание, но в нем могут быть ошибки, которые я предлагаю вам указать, и я исправлю как / если Я нахожу любой.
источник
Карринг (частичное применение) позволяет создать новую функцию из существующей функции, исправив некоторые параметры. Это особый случай лексического замыкания, когда анонимная функция является просто тривиальной оболочкой, которая передает некоторые захваченные аргументы другой функции. Мы также можем сделать это, используя общий синтаксис для создания лексических замыканий, но частичное применение обеспечивает упрощенный синтаксический сахар.
Вот почему программисты на Лиспе, работая в функциональном стиле, иногда используют библиотеки для частичного применения .
Вместо того
(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"
ошибку, результат является функцией оставшихся аргументов.источник
a -> b -> c
не имеет параметр s ( во множественном числе), он имеет только один параметр,c
. При вызове возвращает функцию типаa -> b
.Для функции
Это имеет форму
f': 'a * 'b -> 'c
Чтобы оценить один будет делать
Для карри функции
Чтобы оценить один будет делать
Где это частичное вычисление, в частности (3 + y), которое затем можно завершить вычислением
добавить во втором случае имеет вид
f: 'a -> 'b -> 'c
То, что здесь происходит, - это преобразование функции, которая принимает два соглашения, в одно, которое принимает только одно, возвращающее результат. Частичная оценка
Скажем,
x
RHS - это не просто обычное целое, а сложное вычисление, которое занимает некоторое время, для увеличения, ради двух секунд.Так что функция теперь выглядит так
Типа
add : int * int -> int
Теперь мы хотим вычислить эту функцию для диапазона чисел, давайте отобразим ее
Для вышеизложенного результат
twoSecondsComputation
оценивается каждый раз. Это означает, что это вычисление занимает 6 секунд.Используя комбинацию постановки и карри, можно избежать этого.
Карри
add : int -> int -> int
Теперь можно сделать,
В
twoSecondsComputation
только нужно оценивать один раз. Чтобы увеличить масштаб, замените две секунды на 15 минут или любой час, а затем сопоставьте 100 чисел.Резюме : Карринг отлично подходит при использовании с другими методами для функций более высокого уровня в качестве инструмента частичной оценки. Его цель не может быть продемонстрирована сама по себе.
источник
Карри позволяет гибкую функцию композиции.
Я составил функцию «карри». В этом контексте мне все равно, какой регистратор я получу или откуда он взялся. Мне все равно, что это за действие или откуда оно берется. Все, что меня волнует, - это обработка моего ввода.
Переменная построителя - это функция, которая возвращает функцию, которая возвращает функцию, которая принимает мой ввод и выполняет мою работу. Это простой полезный пример, а не видимый объект.
источник
Карринг является преимуществом, когда у вас нет всех аргументов для функции. Если вам случится полностью оценить функцию, то нет существенной разницы.
Карринг позволяет избежать упоминания пока не нужных параметров. Это более кратко, и не требует нахождения имени параметра, которое не сталкивается с другой переменной в области (что является моим любимым преимуществом).
Например, при использовании функций, которые принимают функции в качестве аргументов, вы часто будете сталкиваться с ситуациями, когда вам нужны такие функции, как «добавить 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>.
источник
Основные рассуждения, которые я могу придумать (и я ни в коем случае не являюсь экспертом в этом вопросе), начинают проявлять свои преимущества по мере того, как функции переходят от тривиальных к нетривиальным. Во всех тривиальных случаях с большинством понятий такого рода вы не найдете никакой реальной выгоды. Однако большинство функциональных языков интенсивно используют стек в операциях обработки. Рассмотрим PostScript или Lisp в качестве примеров этого. Используя карри, функции могут быть сложены более эффективно, и это преимущество становится очевидным, поскольку операции становятся все менее и менее тривиальными. Таким образом, команда и аргументы могут быть выброшены в стек по порядку и вытолкнуты по мере необходимости, чтобы они выполнялись в правильном порядке.
источник
Карринг в решающей степени зависит от способности вернуть функцию.
Рассмотрим этот (надуманный) псевдокод.
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
То, что вы можете частично применить аргументы и получить обратно многократно используемую функцию (привязанную к тем аргументам, которые вы предоставили), весьма полезно (и СУХОЙ).
источник