Понимание функций высшего порядка
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 , чтобы понять разницу.