Понимание сигнатур типа Haskell
Я участвую в обучении самого Хаскелла, и мне было интересно узнать о следующих типах подписей:
Prelude> :t ($)
($) :: (a -> b) -> a -> b
Prelude>
Как я должен интерпретировать (не каламбур), что?
Полупольный результат также оказывается загадочным:
Prelude> :t map
map :: (a -> b) -> [a] -> [b]
Prelude>
Ответы
Ответ 1
Начну с map
. Функция map
применяет операцию к каждому элементу в списке. Если бы у меня был
add3 :: Int -> Int
add3 x = x + 3
Тогда я мог бы применить это ко всему списку Int
с помощью map
:
> map add3 [1, 2, 3, 4]
[4, 5, 6, 7]
Итак, если вы посмотрите на подпись типа
map :: (a -> b) -> [a] -> [b]
Вы увидите, что первый аргумент (a -> b)
, который является просто функцией, которая принимает a
и возвращает b
. Второй аргумент [a]
, который представляет собой список значений типа a
, а возвращаемый тип [b]
- список значений типа b
. Таким образом, на английском языке функция map
применяет функцию к каждому элементу в списке значений, а затем возвращает эти значения в виде списка.
Это то, что делает map
функцию более высокого порядка, она принимает функцию в качестве аргумента и делает все с ней. Другой способ взглянуть на map
- добавить некоторые скобки к сигнатуре типа, чтобы сделать его
map :: (a -> b) -> ([a] -> [b])
Таким образом, вы можете думать об этом как о функции, которая преобразует функцию от a
в b
в функцию от [a]
до [b]
.
Функция ($)
имеет тип
($) :: (a -> b) -> a -> b
И используется как
> add3 $ 1 + 1
5
Все, что он делает, - это то, что справа, в этом случае 1 + 1
, и передает его функции слева, здесь add3
. Почему это важно? Он имеет удобную фиксацию или приоритет оператора, что делает его эквивалентным
> add3 (1 + 1)
Итак, все, что угодно справа, по существу завернуто в круглые скобки, прежде чем будет передано влево. Это просто делает его полезным для объединения нескольких функций вместе:
> add3 $ add3 $ add3 $ add3 $ 1 + 1
лучше, чем
> add3 (add3 (add3 (add3 (1 + 1))))
потому что вам не нужно закрывать круглые скобки.
Ответ 2
Ну, как уже говорилось, $
может быть легко понято, если вы просто забыли про currying и увидите его как, скажем, в С++
template<typename A, typename B>
B dollar(std::function<B(A)> f, A x) {
return f(x);
}
Но на самом деле для этого есть нечто большее, чем просто применение функции к значению! Очевидное сходство между сигнатурами $
и map
имеет на самом деле довольно глубокое значение теории категорий: оба являются примерами морфизма-действия функтора!
В категории Hask, с которой мы работаем все время, объекты являются типами. (Это немного смехотворно, но не беспокойтесь). Морфизмы являются функциями.
Наиболее известными (endo-) функторами являются те, у которых есть экземпляр класс одноименного типа. Но на самом деле, математически, функтор - это только то, что сопоставляет оба объекта с объектами и морфизмами с морфизмами 1. map
(каламбур, я полагаю!) является примером: он принимает объект (т.е. тип) A
и сопоставляет его с типом [A]
. И для любых двух типов A
и B
он принимает морфизм (т.е. Функцию) A -> B
и сопоставляет его с соответствующей функцией списка типа [A] -> [B]
.
Это просто частный случай работы с сигнатурой класса функтора:
fmap :: Functor f => (a->b) -> (f a->f b)
Математика не требует, чтобы этот fmap
имел имя. И поэтому может существовать и тождественный функтор, который просто присваивает себе какой-либо тип. И каждый морфизм себе:
($) :: (a->b) -> (a->b)
"Идентичность" существует, очевидно, более широко, вы также можете сопоставлять значения любого типа себе.
id :: a -> a
id x = x
И, конечно же, возможная реализация тогда
($) = id
1 Разум, а не что-либо, что отображает объекты и морфизмы, является функтором... ему необходимо выполнить законы функтора.
Ответ 3
($)
- это просто приложение. Он получает функцию типа a->b
, аргумент типа a
, применяет эту функцию и возвращает значение типа b
.
map
- прекрасный пример того, как чтение сигнатуры типа функции помогает понять ее. map
Первый аргумент - это функция, которая принимает a
и возвращает b
, а второй аргумент - это список типа [a]
.
Поэтому map
применяет функцию типа a->b
к списку значений a
. И тип результата действительно имеет тип [b]
- список значений b
!
(a->b)->[a]->[b]
можно интерпретировать как "Принимает функцию и список и возвращает другой список", а также как "Принимает функцию типа a->b
и возвращает другую функцию типа [a]->[b]
".
Когда вы смотрите на это таким образом, map
"upgrade" f (термин "лифтинг" часто используется в этом контексте) для работы над списками: if double
- это функция, которая удваивает целое число, тогда map double
функция, которая удваивает каждое целое число в списке.