Первая интересная вещь заключается в том, что a -> b
может поддерживать map
. Да, функции — это функторы!
Рассмотрим тип map
:
map :: Functor f => (b -> c) -> f b -> f c
Давайте заменим Functor f => f
на Array
, чтобы получить конкретный тип:
map :: (b -> c) -> Array b -> Array c
На этот раз заменим Functor f => f
на Maybe
:
map :: (b -> c) -> Maybe b -> Maybe c
Корреляция ясна. Давайте заменим Functor f => f
на Either a
, чтобы проверить бинарный тип:
map :: (b -> c) -> Either a b -> Either a c
Мы часто представляем тип функции от a
до b
как a -> b
, но на самом деле это просто сахар для Function a b
. Давайте воспользуемся длинной формой и заменим Either
в подписи выше на Function
:
map :: (b -> c) -> Function a b -> Function a c
Таким образом, сопоставление функции дает нам функцию, которая применяет функцию b -> c
к возвращаемому значению исходной функции. Мы могли бы переписать подпись, используя сахар a -> b
:
map :: (b -> c) -> (a -> b) -> (a -> c)
Заметили что-нибудь? Каков тип compose
?
compose :: (b -> c) -> (a -> b) -> a -> c
Таким образом, compose
просто map
специализировано для типа Function!
Вторая интересная вещь заключается в том, что a -> b
может поддерживать ap
. Функции также являются аппликативными функторами! Они известны как Apply в спецификации Fantasy Land.
Рассмотрим тип ap
:
ap :: Apply f => f (b -> c) -> f b -> f c
Заменим Apply f => f
на Array
:
ap :: Array (b -> c) -> Array b -> Array c
Теперь с Either a
:
ap :: Either a (b -> c) -> Either a b -> Either a c
Теперь с Function a
:
ap :: Function a (b -> c) -> Function a b -> Function a c
Что такое Function a (b -> c)
? Это немного сбивает с толку, потому что мы смешиваем два стиля, но это функция, которая принимает значение типа a
и возвращает функцию от b
до c
. Перепишем в стиле a -> b
:
ap :: (a -> b -> c) -> (a -> b) -> (a -> c)
Можно поднять любой тип, который поддерживает map
и ap
. Давайте посмотрим на lift2
:
lift2 :: Apply f => (b -> c -> d) -> f b -> f c -> f d
Помните, что Function a
удовлетворяет требованиям Apply, поэтому мы можем заменить Apply f => f
на Function a
:
lift2 :: (b -> c -> d) -> Function a b -> Function a c -> Function a d
Что более четко написано:
lift2 :: (b -> c -> d) -> (a -> b) -> (a -> c) -> (a -> d)
Давайте вернемся к вашему исходному выражению:
// average :: Number -> Number
const average = lift2(divide, sum, length);
Что делает average([6, 7, 8])
? a
([6, 7, 8]
) передается функции a -> b
(sum
), в результате чего получается b
(21
). a
также передается функции a -> c
(length
), производящей c
(3
). Теперь, когда у нас есть b
и c
, мы можем передать их функции b -> c -> d
(divide
), чтобы получить d
(7
), что является конечным результатом.
Итак, поскольку тип Function может поддерживать map
и ap
, мы получаем converge
бесплатно (через lift
, lift2
и lift3
). На самом деле я бы хотел удалить converge
из Ramda, так как в этом нет необходимости.
Обратите внимание, что я намеренно не использовал в этом ответе R.lift
. Имеет бессмысленную сигнатуру типа и сложную реализацию из-за решения поддерживать функции любой арности. С другой стороны, специфичные для арности функции подъема Sanctuary имеют четкие сигнатуры типов и тривиальные реализации.
person
davidchambers
schedule
17.09.2016