Подтвердить что ты не робот

Понимание сигнатур типа Haskell

Я участвую в обучении самого Хаскелла, и мне было интересно узнать о следующих типах подписей:

Prelude> :t ($)
($) :: (a -> b) -> a -> b
Prelude>

Как я должен интерпретировать (не каламбур), что?

Полупольный результат также оказывается загадочным:

Prelude> :t map
map :: (a -> b) -> [a] -> [b]
Prelude>
4b9b3361

Ответ 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 функция, которая удваивает каждое целое число в списке.