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

Я очень новичок в 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,

6 ответов

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

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 с использованием схемы в качестве примера.)

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

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

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

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

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

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

Вот почему синтаксис приложения функции в Haskell не имеет скобок и запятых, сравните Haskell f a b c с Python или Java f(a, b, c), Это не какое-то странное эстетическое решение, в приложении функции 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 любая функция по умолчанию каррируется и поэтому всегда принимает один аргумент, поэтому разделы могут фактически использоваться с любой функцией: let (+^) быть какой-то странной функцией, которая суммирует 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, чтобы понять разницу.

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

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

Это помогает?

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

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

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

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

Что-то, что может помочь, - подумать о том, как вы могли бы реализовать карри как функцию более высокого порядка, если бы 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, если вы хотите карри), потому что поддержка встроена в язык.

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

function add($a) {
    return function($b) use($a) {
         return $a + $b;
    };
}

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

      (+) :: Num a => a -> a -> a
(+) x y = \x -> (\y -> x + y) 

То есть,(+) :: a -> a -> aпо сути такой же, как(+) :: a -> (a -> a). Надеюсь это поможет.

Другие вопросы по тегам