Что особенного в карри или частичном применении?

9

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

Возьмем этот Groovy-код в качестве примера:

def mul = { a, b -> a * b }
def tripler1 = mul.curry(3)
def tripler2 = { mul(3, it) }

Я не понимаю, в чем разница между tripler1и tripler2. Разве они не одинаковы? 'Curry' поддерживается в чистых или частично функциональных языках, таких как Groovy, Scala, Haskell и т. Д. Но я могу сделать то же самое (левое карри, правое карри, n-карри или частичное приложение), просто создав другое именованное или анонимное функция или замыкание, которое будет перенаправлять параметры в исходную функцию (например tripler2) на большинстве языков (даже на C.)

Я что-то здесь упускаю? Есть места, где я могу использовать карри и частичную аппликацию в своем приложении Grails, но я не решаюсь сделать это, потому что спрашиваю себя: "Как это отличается?"

Пожалуйста, просветите меня.

РЕДАКТИРОВАТЬ: Вы, ребята, говорите, что частичное применение / каррирование просто более эффективно, чем создание / вызов другой функции, которая перенаправляет параметры по умолчанию в исходную функцию?

Vigneshwaran
источник
1
Может кто-нибудь создать теги "карри" или "карри"?
Vigneshwaran
Как вы карри в C?
Джорджио
это, вероятно, действительно больше о частичном применении programmers.stackexchange.com/questions/152868/…
jk.
1
@Vigneshwaran: AFAIK, вам не нужно создавать другую функцию на языке, поддерживающем карри. Например, в Haskell f x y = x + yозначает, что fэто функция, которая принимает один параметр int. Результатом f x( fприменяется к x) является функция, которая принимает один параметр типа int. Результат f x y(или (f x) y, т. f xЕ. Примененный к y) является выражением, которое не принимает входных параметров и оценивается путем сокращения x + y.
Джорджио
1
Вы можете достичь того же, но количество усилий, которое вы делаете с помощью C, гораздо более болезненно и не так эффективно, как в языке, подобном haskell, где это поведение по умолчанию
Даниэль Гратцер,

Ответы:

8

Карринг - это превращение / представление функции, которая принимает n входов в n функций, каждая из которых принимает 1 вход. Частичное применение - это исправление некоторых входных данных для функции.

Мотивация для частичного применения заключается, прежде всего, в том, что это облегчает написание библиотек функций более высокого порядка. Например, все алгоритмы в C ++ STL в основном принимают предикаты или унарные функции, bind1st позволяет пользователю библиотеки подключать не унарные функции с ограниченным значением. Поэтому создателю библиотеки не нужно предоставлять перегруженные функции для всех алгоритмов, которые принимают унарные функции для предоставления двоичных версий

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

JK.
источник
это curryingчто - то конкретное для Groovy или неприменимыми по языкам?
амфибия
@foampile - это то, что применимо ко всем языкам, но по иронии судьбы карри на самом деле этого не делает programmers.stackexchange.com/questions/152868/…
jk.
@jk. Вы говорите, что каррирование / частичное применение более эффективно, чем создание и вызов другой функции?
Vigneshwaran
2
@Vigneshwaran - он не обязательно более производительный, но он определенно более эффективен с точки зрения времени программиста. Также обратите внимание, что хотя карринг поддерживается многими функциональными языками, но обычно не поддерживается в ОО или процедурных языках. (Или, по крайней мере, не самим языком.)
Стивен С.
6

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

И оптимизатор посмотрит на это и быстро перейдет к тому, что сможет понять. Карринг - это приятный маленький трюк для конечного пользователя, но он имеет гораздо больше преимуществ с точки зрения языкового дизайна. Это действительно приятно обрабатывать все методы как унарные, A -> BгдеB может быть другим способом.

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

Telastyn
источник
6

Как @jk. Выражение карри может помочь сделать код более общим.

Например, предположим, что у вас были эти три функции (в Haskell):

> let q a b = (2 + a) * b

> let r g = g 3

> let f a b = b (a 1)

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

Если бы мы вызывали fс использованием qи rв качестве аргументов, это было бы эффективно:

> r (q 1)

где qбудет применяться 1и возвращать другую функцию (как qкарри); эта возвращаемая функция затем будет передана в rкачестве аргумента для аргумента 3. Результатом этого будет значение 9.

Теперь, скажем, у нас было две другие функции:

> let s a = 3 * a

> let t a = 4 + a

мы могли бы также передать их fи получить значение 7или 15, в зависимости от того, были ли наши аргументы s tили t s. Так как эти функции возвращают значение , а не функции, не частичное применение не будет иметь место в f s tилиf t s .

Если бы мы написали fс qи rв виду , что мы могли бы использовать лямбда (анонимные функции) вместо частичного применения, например:

> let f' a b = b (\x -> a 1 x)

но это ограничило бы общность f'. fможно вызывать с аргументами qи rили sи t, но f'можно вызывать только с qи r- f' s tиf' t s оба приводят к ошибке.

БОЛЬШЕ

Если бы он f'вызывался с парой q'/, r'где q'аргумент принимает более двух аргументов, q'он все равно будет частично применен в f'.

В качестве альтернативы, вы можете обернуть qснаружи, fа не внутри, но это оставит вас с неприятной вложенной лямбдой:

f (\x -> (\y -> q x y)) r

что по сути то, что карри qбыл на первом месте!

Павел
источник
Ты открыл мне глаза. Ваш ответ заставил меня понять, чем карри / частично применяемые функции отличаются от создания новой функции, которая передает аргументы исходной функции. 1. Передача каррированных / платных функций (таких как f (q.curry (2))) удобнее, чем ненужное создание отдельных функций для временного использования. (В функциональных языках, таких как groovy)
Vigneshwaran
2. На мой вопрос я сказал: «Я могу сделать то же самое в C.» Да, но в нефункциональных языках, где вы не можете передавать функции как данные, создание отдельной функции, которая перенаправляет параметры в оригинал, не имеет всех преимуществ каррирования / pa
Vigneshwaran
Я заметил, что Groovy не поддерживает тот тип обобщения, который поддерживает Haskell. Я должен был написать, def f = { a, b -> b a.curry(1) }чтобы заставить f q, rработать def f = { a, b -> b a(1) }или или def f = { a, b -> b a.curry(1)() }для f s, tработы. Вы должны передать все параметры или явно сказать, что вы карри. :(
Vigneshwaran
2
@Vigneshwaran: Да, можно с уверенностью сказать, что Haskell и карри очень хорошо сочетаются . ;] Обратите внимание, что в Haskell функции каррируются (в правильном определении) по умолчанию, а пробел указывает на применение функции, поэтому f x yозначает, что многие языки будут писать f(x)(y), а не f(x, y). Возможно, ваш код будет работать в Groovy, если вы напишите qтак, что он будет называться как q(1)(2)?
CA McCann
1
@ Vigneshwaran Рад, что я мог помочь! Я чувствую вашу боль из-за того, что вам приходится прямо говорить, что вы подаете частичную заявку. В Clojure я должен сделать (partial f a b ...)- будучи привыкшим к Haskell, я очень скучаю по правильному карри при программировании на других языках (хотя я недавно работал на F #, который, к счастью, поддерживает это).
Пол
3

Есть два ключевых момента о частичном применении. Первый - синтаксический / удобный - некоторые определения становятся проще и короче для чтения и записи, как упоминалось в @jk. (Проверьте Pointfree программирования для получения дополнительной информации о том, как это здорово!)

Второй, как упоминал @telastyn, касается модели функций и не просто удобен. В версии на Haskell, из которой я получу свои примеры, потому что я не знаком с другими языками с частичным применением, все функции принимают один аргумент. Да, даже такие функции, как:

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

принять один аргумент; из-за ассоциативности конструктора функционального типа ->вышеприведенное эквивалентно:

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

которая является функцией, которая принимает aи возвращает функцию [a] -> [a].

Это позволяет нам писать такие функции, как:

($) :: (a -> b) -> a -> b

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

f :: (t, t1) -> t -> t1 -> (t2 -> t3 -> (t, t1)) -> t2 -> t3 -> [(t, t1)]
f q r s t u v = q : (r, s) : [t u v]

f' :: () -> Char -> (t2 -> t3 -> ((), Char)) -> t2 -> t3 -> [((), Char)]
f' = f $ ((), 'a')  -- <== works fine

Ладно, это был надуманный пример. Но более полезный включает класс типов Applicative , который включает этот метод:

(<*>) :: Applicative f => f (a -> b) -> f a -> f b

Как видите, тип идентичен аналогичному, $если вы убираете Applicative fбит, и фактически этот класс описывает применение функции в контексте. Итак, вместо обычного приложения функции:

ghci> map (+3) [1..5]  
[4,5,6,7,8]

Мы можем применять функции в аппликативном контексте; например, в контексте Maybe, в котором что-то может присутствовать или отсутствовать:

ghci> Just map <*> Just (+3) <*> Just [1..5]
Just [4,5,6,7,8]

ghci> Just map <*> Nothing <*> Just [1..5]
Nothing

Теперь действительно прохладная часть является то , что Прикладное класс типа не упоминает ничего о функциях более одного аргумента - тем не менее, он может иметь дело с ними, даже функциями 6 аргументов , такими как f:

fA' :: Maybe (() -> Char -> (t2 -> t3 -> ((), Char)) -> t2 -> t3 -> [((), Char)])
fA' = Just f <*> Just ((), 'a')

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


источник
1
Applicativeбез карри или частичного применения будет использовать fzip :: (f a, f b) -> f (a, b). В языке с функциями более высокого порядка это позволяет поднять каррирование и частичное применение в контекст функтора и эквивалентно (<*>). Без функций высшего порядка у вас не будет, fmapпоэтому все это будет бесполезно.
CA Макканн
@CAMcCann спасибо за отзыв! Я знал, что был над моей головой с этим ответом. Так что я сказал не так?
1
Это правильно по духу, конечно. Расщепление волос на определения «общая форма», «возможный» и «концепция частичного применения» не изменит тот простой факт, что очаровательный f <$> x <*> yидиоматический стиль работает легко, потому что карри и частичное применение работают легко. Другими словами, то, что приятно, важнее, чем то, что здесь возможно .
CA McCann
Каждый раз, когда я вижу примеры кода функционального программирования, я все больше убеждаюсь, что это сложная шутка и ее нет.
Kieveli
1
@Kieveli жаль, что ты так себя чувствуешь. Есть много хороших учебных пособий , которые помогут вам разобраться с основами.