Как работает карри? - PullRequest
       7

Как работает карри?

13 голосов
/ 11 июля 2011

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

Вот функция: (+) :: a -> (a -> a) Если я сделаю (+) 4 7,Функция принимает 4 и возвращает функцию, которая принимает 7 и возвращает 11.Но что происходит с 4?Что эта первая функция делает с 4?Что (a -> a) делает с 7?

Все становится более запутанным, когда я думаю о более сложной функции:

max' :: Int -> (Int -> Int)
max' m n | m > n = m
         | otherwise = n

с чем (Int -> Int) сравнивает свой параметр?Требуется только один параметр, но для него нужно два m > n.

Ответы [ 5 ]

13 голосов
/ 11 июля 2011

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

12 голосов
/ 11 июля 2011

Помогает ли это?

max' = \m -> \n -> if (m > n)
                       then m
                       else n

Написано в виде лямбды.max '- это значение лямбды, которое само возвращает лямбду с заданным значением m, которое возвращает значение.

Следовательно, max' 4 равно

max' 4 = \n -> if (4 > n)
                   then 4
                   else n
10 голосов
/ 19 февраля 2015

Понимание функций высшего порядка

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

Но что, если сами функции ничем не отличаются от значений, и вы можете принять функцию в качестве аргумента или вернуть ее из другой функции? f a b c = a + b - c - скучная функция, она суммирует a и b, а затем вычитает c. Но функция могла бы быть более интересной, если бы мы могли обобщить ее, что, если бы мы хотели иногда суммировать a и b, но иногда умножать? Или разделить на c вместо вычитания?

Помните, (+) - это просто функция 2 чисел, которая возвращает число, в этом нет ничего особенного, поэтому любая функция из 2 чисел, которая возвращает число, может быть вместо нее. Написание g a b c = a * b - c, h a b c = a + b / c и т. Д. Для нас просто не значит, нам нужно общее решение, ведь мы программисты! Вот как это делается в Haskell:

let f g h a b c = a `g` b `h` c in f (*) (/) 2 3 4 -- returns 1.5

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

let g f n = (\m -> m `f` n); f = g (+) 2 in f 10 -- returns 12

Конструкция (\m -> m `f` n) - это анонимная функция с 1 аргументом m, которая применяет f к этим m и n. По сути, когда мы вызываем g (+) 2, мы создаем функцию с одним аргументом, которая просто добавляет 2 к тому, что получает. Так что let f = g (+) 2 in f 10 равно 12 и let f = g (*) 5 in f 5 равно 25.

(См. Также мое объяснение HOF на примере схемы.)

Понимание карри

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

Теперь представьте, что вы могли бы дать ему только 1 аргумент, и он вернул бы функцию? Вы можете использовать эту функцию позже, чтобы добавить этот аргумент 1 st , теперь включенный в эту новую функцию, к чему-то еще. E.g.:

f n = (\m -> n - m)
g = f 10
g 8 -- would return 2
g 4 -- would return 6

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

Это видно по типам. Напишите :t (++) в интерпретаторе, где (++) - это функция, которая объединяет 2 строки вместе, и она вернет (++) :: [a] -> [a] -> [a]. Тип не [a],[a] -> [a], а [a] -> [a] -> [a], что означает, что (++) принимает один список и возвращает функцию типа [a] -> [a]. Эта новая функция может принимать еще один список и, наконец, вернет новый список типа [a].

Поэтому синтаксис приложения функции в Haskell не имеет скобок и запятых, сравните f a b c на Haskell с f(a, b, c) на Python или Java. Это не какое-то странное эстетическое решение, в приложении функции Haskell происходит слева направо, поэтому f a b c на самом деле (((f a) b) c), что имеет смысл, если вы знаете, что f по умолчанию карри.

Однако в типах связь происходит справа налево, поэтому [a] -> [a] -> [a] эквивалентно [a] -> ([a] -> [a]). Они одинаковы в Haskell, Haskell относится к ним точно так же. Это имеет смысл, потому что когда вы применяете только один аргумент, вы получаете функцию типа [a] -> [a].

С другой стороны, проверьте тип map: (a -> b) -> [a] -> [b], он получает функцию в качестве первого аргумента, и поэтому имеет круглые скобки.

Чтобы по-настоящему выработать концепцию каррирования, попробуйте найти типы следующих выражений в интерпретаторе:

(+)
(+) 2
(+) 2 3
map
map (\x -> head x)
map (\x -> head x) ["conscience", "do", "cost"]
map head
map head ["conscience", "do", "cost"]

Частичное применение и разделы

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

Вы уже понимаете, что вместо создания анонимных функций вы можете просто частично применить функцию, поэтому вместо написания (\x -> replicate 3 x) вы можете просто написать (replicate 3). Но что, если вы хотите использовать оператор деления (/) вместо replicate? Для инфиксных функций Haskell позволяет частично применить его, используя любой из аргументов.

Это называется секции : (2/) эквивалентно (\x -> 2 / x) и (/2) эквивалентно (\x -> x / 2). С помощью обратных кавычек вы можете взять раздел любой двоичной функции: (2`elem`) эквивалентно (\xs -> 2 `elem` xs).

Но помните, что любая функция в Haskell по умолчанию каррируется и поэтому всегда принимает один аргумент, поэтому разделы могут фактически использоваться с любой функцией: пусть (+^) будет какой-то странной функцией, которая суммирует 4 аргумента, тогда let (+^) a b c d = a + b + c in (2+^) 3 4 5 возвращает 14 .

Составы

Другими удобными инструментами для написания краткого и гибкого кода являются композиция и оператор приложения . Оператор композиции (.) объединяет функции. Оператор приложения ($) просто применяет функцию слева к аргументу справа, поэтому f $ x эквивалентно f x. Однако ($) имеет самый низкий приоритет среди всех операторов, поэтому мы можем использовать его, чтобы избавиться от скобок: f (g x y) эквивалентно f $ g x y.

Также полезно, когда нам нужно применить несколько функций к одному и тому же аргументу: map ($2) [(2+), (10-), (20/)] даст [4,8,10]. (f . g . h) (x + y + z), f (g (h (x + y + z))), f $ g $ h $ x + y + z и f . g . h $ x + y + z эквивалентны, но (.) и ($) - разные вещи, поэтому читайте Haskell: разница между. (точка) и $ (знак доллара) и части из Learn You a Haskell , чтобы понять разницу.

3 голосов
/ 12 июля 2011

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

function add($a) {
    return function($b) use($a) {
         return $a + $b;
    };
}
3 голосов
/ 11 июля 2011

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

curry :: (a -> b -> c) -> a -> (b -> c)
curry f a = \b -> f a b

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

В ghci:

Prelude> let curry f a = \b -> f a b
Prelude> let g = curry (+) 5
Prelude> g 10
15
Prelude> g 15
20
Prelude> 

К счастью, нам не нужно делать это в Haskell (вы делаете в Lisp, если хотите карри), потому что поддержка встроена в язык.

...