2012-09-13 5 views
8

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

+2

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

+2

Вы понимаете преимущества лямбда-функций? –

+0

@ DaxFohl как в C# lambdas? – user997112

ответ

12

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

Место где я понял преимущества первого, когда я увидел отрезанные операторы:

incElems = map (+1) 
--non-curried equivalent: incElems = (\elems -> map (\i -> (+) 1 i) elems) 

ИМО, это совершенно легко читать. Теперь, если тип (+) был (Int,Int) -> Int *, который является неопушенной версией, он будет (встречно-интуитивно) приводить к ошибке - но curryied, он работает как ожидалось и имеет тип [Int] -> [Int].

Вы упомянули C# lambdas в комментарии. В C#, вы могли бы написать incElems как так, задан функции plus:

var incElems = xs => xs.Select(x => plus(1,x)) 

Если вы привыкли к точечным свободному стилю, вы увидите, что x здесь является излишним. Логически, что код может быть уменьшен до

var incElems = xs => xs.Select(curry(plus)(1)) 

, который ужасно из-за отсутствия автоматического частичного применения с C# лямбдах. И это решающий момент, чтобы решить, где каррирование действительно полезно: в основном, когда это происходит неявно.Для меня map (+1) проще всего читать, затем приходит .Select(x => plus(1,x)), и, вероятно, следует избегать версии с curry, если нет действительно веской причины.

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

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

* Конечно, это действительно в Num, но на данный момент это более читаемо.


Обновление: как работает каррирование.

Посмотрите на тип plus в C#:

int plus(int a, int b) {..} 

Вы должны дать ему кортеж значений - не в терминах C#, но математически разговорного; вы не можете просто оставить второе значение. В Haskell выражении это

plus :: (Int,Int) -> Int, 

, которые могут быть использованы как

incElem = map (\x -> plus (1, x)) -- equal to .Select (x => plus (1, x)) 

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

curry f = \x -> (\y -> f (x,y)) 
plus' = curry plus 

который дает

incElem = map (plus' 1) 

Давайте применим это к конкретному значению.

incElem [1] 
= (map (plus' 1)) [1] 
= [plus' 1 1] 
= [(curry plus) 1 1] 
= [(\x -> (\y -> plus (x,y))) 1 1] 
= [plus (1,1)] 
= [2] 

Здесь вы можете увидеть curry на работе. Он превращает стандартное приложение функции стиля в стиле haskell (plus' 1 1) в вызов функции «пополнения» - или, если смотреть на более высоком уровне, преобразует «пополняемый» в «необработанную» версию.

К счастью, в большинстве случаев вам не о чем беспокоиться, так как есть автоматическое частичное применение.

+0

Просто, чтобы уточнить, почему (+1) каррирование? Я просто хочу быть уверенным, что я точно понимаю, что это такое, а не только почему это хорошо. Спасибо – user997112

+0

@ user997112: '(+ 1)' - это частичное приложение, использующее специальный синтаксис для инфиксных операторов. Если '(+)' были вызваны вместо 'add', currying позволит вам написать' add 1 1' вместо 'add (1, 1)'. Частично применение последнего гораздо более неудобно, как объясняет вышеприведенный ответ. –

+1

@ user997112 - '(+1)' - это частичное приложение - '(+)' - это валютная функция, так как она имеет тип 'Int -> Int -> Int' вместо' (Int, Int) -> Int', поскольку например, в C#. В Haskell вы можете преобразовать '(+)' во вторую форму, используя 'uncurry', например. uncurry (+) $ (1,1) – Lee

4

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

mapLength = map length 
mapLength ["ab", "cde", "f"] 
>>> [2, 3, 1] 
mapLength ["x", "yz", "www"] 
>>> [1, 2, 3] 

map :: (a -> b) -> [a] -> [b] 
length :: [a] -> Int 
mapLength :: [[a]] -> [Int] 

map функция может рассматриваться как имеющий тип (a -> b) -> ([a] -> [b]) из-за выделки, поэтому, когда length применяется в качестве первого аргумента, она дает функцию mapLength типа [[a]] -> [Int].

+0

Какая часть вашего кода является currying? Вы имеете в виду, что определение карты (или длины) содержит ее в прелюдии? – user997112

+1

@ user997112 'map' - это каррическая функция. Вот почему можно написать «длину карты», чтобы частично применить ее (в отличие от '\ xs -> map (length, xs)'), которую вы должны были бы написать, если бы она была определена с использованием кортежей, а не каррирования). – sepp2k

+0

@ user997112 Уточненный в ответе. –

7

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

map (max 4) [0,6,9,3] --[4,6,9,4] 
map (\i -> max 4 i) [0,6,9,3] --[4,6,9,4] 

Этих виды конструкций придумали достаточно часто, когда вы используете функциональное программирование, что это хороший ярлык, чтобы иметь и позволяет думать о проблеме с несколько более высокого уровня - you're отображение против функции «max 4», а не какой-либо случайной функции, которая определяется как (\i -> max 4 i). Это позволяет начать более легко думать, на более высоких уровнях косвенности:

let numOr4 = map $ max 4 
let numOr4' = (\xs -> map (\i -> max 4 i) xs) 
numOr4 [0,6,9,3] --ends up being [4,6,9,4] either way; 
       --which do you think is easier to understand? 

Тем не менее, это не является панацеей; иногда параметры вашей функции будут неправильным порядком для того, что вы пытаетесь сделать с currying, так что вам придётся прибегать к лямбде в любом случае. Однако, как только вы привыкнете к этому стилю, вы начинаете изучать, как проектировать ваши функции, чтобы хорошо работать с ним, и как только эти нейроны начинают соединяться внутри вашего мозга, ранее сложные конструкции могут казаться очевидными в сравнении.

+1

О, вы никогда не имеете * прибегать к лямбда (но вам часто нужно). –

+0

Я не понимаю этот комментарий? – user997112

+3

@ user997112 вы можете использовать 'flip' и друзей для создания новых функций из существующих функций с изменением порядка параметров. Итак, если у вас есть 'f :: x-> y-> z',' (flip f) y1' будет эквивалентно '(\ x -> fx y1)', что позволит вам «карри» 2-го параметр. Но это может (как правило, делает) в конечном итоге продуцировать код гораздо более неясным, чем просто использовать лямбда. –

2

Это несколько сомнительно, чтобы спросить, что выгоды от выделки находятся без указания контекста, в котором вы просите вопрос:

  • В некоторых случаях, например функциональных языках, выделка только будет рассматриваться как то, что имеет более локальное изменение, где вы можете заменить вещи явными корневыми доменами. Однако это не означает, что каррирование на этих языках бесполезно. В некотором смысле, программирование с помощью карриных функций делает вас «чувствовать», как будто вы программируете в более функциональном стиле, потому что чаще вы сталкиваетесь с ситуациями, когда вы имеете дело с функциями более высокого порядка. Конечно, большую часть времени вы будете «заполнять» все аргументы функции, но в тех случаях, когда вы хотите использовать функцию в своей частично применяемой форме, это немного проще сделать в карри.Обычно мы говорим нашим начинающим программистам об использовании этого при изучении функционального языка только потому, что он чувствует себя как лучший стиль и напоминает, что он программирует больше, чем просто C. Имея такие вещи, как curry и uncurry, также помогайте некоторым удобствам в языках функционального программирования , Я могу думать о стрелках внутри Haskell в качестве конкретного примера, где вы бы использовали curry и uncurry немного, чтобы применить вещи к разным частям стрелки и т. Д.
  • В некоторых случаях вы хотите думать о более чем функциональные программы, вы можете представить currying/uncurrying как способ изложения правил исключения и введения для и в конструктивной логике, которая обеспечивает связь с более элегантной мотивацией для того, почему она существует.
  • В некоторых случаях, например, в Coq с использованием карриных функций по сравнению с чередующимися функциями могут возникать различные схемы индукции, с которыми может быть проще или сложнее работать, в зависимости от ваших приложений.
3

Currying имеет функции удобства, упомянутые в других ответах, но также часто служит для упрощения рассуждений о языке или для реализации некоторого кода намного проще, чем в противном случае. Например, currying означает, что любая функция вообще имеет тип, совместимый с a ->b. Если вы напишете какой-то код, тип которого включает a -> b, этот код может быть выполнен с любой функцией вообще, независимо от того, сколько аргументов оно принимает.

Наиболее известным примером этого является Applicative класс:

class Functor f => Applicative f where 
    pure :: a -> f a 
    (<*>) :: f (a -> b) -> f a -> f b 

И пример использования:

-- All possible products of numbers taken from [1..5] and [1..10] 
example = pure (*) <*> [1..5] <*> [1..10] 

В этом контексте pure и <*> адаптировать любую функцию типа a -> b работать с списки типа [a]. Из-за частичного применения это означает, что вы также можете адаптировать функции типа a -> b -> c для работы с [a] и [b] или a -> b -> c -> d с [a], [b] и [c] и так далее.

Причина это работает, потому что a -> b -> c это то же самое, как a -> (b -> c):

(+)        :: Num a => a -> a -> a 
pure (+)      :: (Applicative f, Num a) => f (a -> a -> a) 
[1..5], [1..10]     :: Num a => [a] 
pure (+) <*> [1..5]    :: Num a => [a -> a] 
pure (+) <*> [1..5] <*> [1..10] :: Num a => [a] 

Другой, иное использование карирования является то, что Haskell позволяет частично применять конструкторы типов. Например, если у вас есть этот тип:

data Foo a b = Foo a b 

... это на самом деле имеет смысл писать Foo a во многих контекстах, например:

instance Functor (Foo a) where 
    fmap f (Foo a b) = Foo a (f b) 

Ie, Foo является конструктором типа двухпараметрического с вид * -> * -> *; Foo a, частичное применение Foo к одному типу, является конструктором типа с видом * -> *. Functor - тип типа, который может быть создан только для типов конструкторов типа * -> *. Так как Foo a такого типа, вы можете сделать для него Functor.

+0

Хорошая точка. Я думаю, что это то, что я пытался сказать об исчислении лямбда: currying позволяет использовать язык, который поддерживает только однозначные функции, как если бы он имел многозначные функции. – phg

+0

@phg, так что не выполняются (т. Е. Загруженные) функции. –

3

«ничто не-карринг» форма частичного применения работает следующим образом:

  • У нас есть функция f : (A ✕ B) → C
  • Мы хотели бы, чтобы применить его частично к некоторым a : A
  • Чтобы сделать это, мы строим закрытие из a и f (мы пока не оцениваем f)
  • Затем через некоторое время мы получим второй аргумент b : B
  • Теперь, когда у нас есть и A и B аргумент, мы можем оценить f в его первоначальном виде ...
  • Так мы вспомним a от закрытия, и оценить f(a,b).

Немного сложный, не так ли?

Когда f является кэрри в первую очередь, это довольно простой:

  • У нас есть функция f : A → B → C
  • Мы хотели бы, чтобы применить его частично к некоторым a : A - что мы можем просто сделать : f a
  • Затем через некоторое время, мы получаем второй аргумент b : B
  • Применим уже оценили f a - b.

До сих пор так хорошо, но что более важно, чем быть просто, это также дает нам дополнительные возможности для реализации нашей функции: мы можем быть в состоянии сделать некоторые расчеты, как только получил a аргумент, и выиграл эти расчеты 't нужно сделать позже, даже если функция оценивается с несколькими различными аргументами b!

Для примера рассмотрим this audio filter, Бесконечный импульсный отклик фильтр. Он работает следующим образом: для каждого аудиоподразделения вы подаете «функцию аккумулятора» (f) с некоторым параметром состояния (в данном случае простым числом, 0 в начале) и звуковым образцом. Затем функция выполняет некоторую магию и выплевывает внутреннее состояние и выходной образец.

Теперь вот решающий бит - какой вид магии функция не зависит от коэффициента λ, что не совсем постоянная: она зависит как от того, что частоты среза мы хотели бы фильтр, чтобы (это управляет «тем, как будет звучать фильтр») и по какой частоте дискретизации мы обрабатываем. К сожалению, расчет λ немного сложнее (lp1stCoeff $ 2*pi * (νᵥ ~*% δs), чем остальная часть магии, поэтому нам не хотелось бы делать это для каждого отдельного образца снова и снова. Довольно раздражает, потому что νᵥ и δsпочти константа: они меняются очень редко, конечно, не на каждом образце аудио.

Но карри спасает день! Мы просто вычислим λ, как только у нас появятся необходимые параметры. Затем, при каждом из множества звуковых образцов, нам нужно только выполнить оставшуюся, очень легкую магию: yⱼ = yⱼ₁ + λ ⋅ (xⱼ - yⱼ₁). Таким образом, мы эффективны и сохраняем хороший безопасный прозрачный чисто функциональный интерфейс.


Обратите внимание, что этот вид государственной обходе в целом можно сделать более красиво с State или ST монады, это просто не особенно полезно в этом примере

Да, это символ лямбда. Надеюсь, я никого не смучу - к счастью, в Haskell ясно, что лямбда-функции написаны с \, а не с λ.

1

Я привык думать, что каррирование было простым синтаксическим сахаром, что экономит вам немного печатать. Например, вместо написания

(\ x -> x + 1) 

Я могу просто написать

(+1) 

Последнее мгновенно более читаемым и менее типирование для загрузки.

Так что, если это просто удобный короткий путь, почему все суеты?

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

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

Смежные вопросы