Ответ 1
С точки зрения программиста сущность функторности способна легко адаптировать вещи. То, что я подразумеваю под "приспосабливанием", заключается в том, что если у меня есть f a
, и мне нужен f b
, мне нужен адаптер, который будет соответствовать моему f a
в моей f b
-образной дыре.
Кажется интуитивным, что если я могу превратить a
в b
, я мог бы превратить a f a
в f b
. И действительно, что образ, который класс Haskell Functor
воплощает; если я поставлю функцию a -> b
, тогда fmap
позволяет мне адаптировать f a
вещи в объекты f b
, не беспокоясь о том, что включает f
. 1
Конечно, речь идет о параметризованных типах, таких как list-of-x [x]
, Maybe y
или IO z
здесь, и то, что мы можем изменить с помощью наших адаптеров, это x
, y
или z
в них. Если нам нужна гибкость для получения адаптера из любой возможной функции a -> b
, то, конечно, мы адаптируемся к тому, чтобы быть в равной степени применимы к любому возможному типу.
Что менее интуитивно (сначала) состоит в том, что существуют некоторые типы, которые могут быть адаптированы почти точно так же, как функциональные, только они "обратные"; для них, если мы хотим адаптировать f a
для заполнения потребности f b
, нам действительно нужно предоставить функцию b -> a
, а не a -> b
one!
Мой любимый конкретный пример - это фактически тип функции a -> r
(a для аргумента, r для результата); вся эта абстрактная глупость имеет смысл при применении к функциям (и если вы сделали какое-либо существенное программирование, вы почти наверняка использовали эти понятия, не зная терминологии или того, насколько широко они применимы), и эти два понятия настолько очевидны двойственные друг к другу в этом контексте.
Хорошо известно, что a -> r
является функтором в r
. Это имеет смысл; если у меня есть a -> r
, и мне нужен a -> s
, тогда я мог бы использовать функцию r -> s
для адаптации моей исходной функции просто путем последующей обработки результата. 2
Если, с другой стороны, у меня есть функция a -> r
, и мне нужно это b -> r
, то снова ясно, что я могу удовлетворить свои потребности, предварительно обработав аргументы, прежде чем передавать их исходной функции. Но с чем я их предварительно обрабатываю? Исходная функция - черный ящик; независимо от того, что я делаю, он всегда ожидает ввода a
. Поэтому мне нужно преобразовать значения b
в значения a
, которые он ожидает: моему адаптеру предварительной обработки нужна функция b -> a
.
Мы только что видели, что тип функции a -> r
является ковариантным функтором в r
и контравариантным функтором в a
. Я думаю об этом, говоря, что мы можем адаптировать результат функции, а тип результата "изменяется с помощью адаптера r -> s
, а когда мы адаптируем аргумент функции, тип аргумента изменяется в" противоположном направлении "к адаптеру.
Интересно, что реализация функции-результата fmap
и функции-аргумента contramap
почти то же самое: просто составная функция (оператор .
)! Единственная разница в том, с какой стороны вы составляете функцию адаптера: 3
fmap :: (r -> s) -> (a -> r) -> (a -> s)
fmap adaptor f = adaptor . f
fmap adaptor = (adaptor .)
fmap = (.)
contramap' :: (b -> a) -> (a -> r) -> (b -> r)
contramap' adaptor f = f . adaptor
contramap' adaptor = (. adaptor)
contramap' = flip (.)
Я считаю, что второе определение из каждого блока наиболее проницательно; (ковариантно) отображение по результату функции - это композиция слева (посткомпозиция, если мы хотим взять представление "это-произойдет-после-того" ), тогда как контравариантное отображение над аргументом функции - это композиция справа (pre- состав).
Эта интуиция довольно хорошо обобщается; если структура f x
может дать нам значения типа x
(как функция a -> r
дает нам r
значения, по крайней мере потенциально), она может быть ковариантной Functor
в x
, и мы может использовать функцию x -> y
, чтобы адаптировать ее к f y
. Но если структура f x
получает от нас значения типа x
(опять же, как и аргумент функции a -> r
типа a
), тогда это может быть функтор Contravariant
, и нам нужно будет использовать y -> x
, чтобы адаптировать его к f y
.
Мне интересно подумать, что эти "источники ковариантны, направления контравариантны" интуиция меняет направление, когда вы думаете с точки зрения разработчика источника/адресата, а не вызывающего. Если я пытаюсь внедрить f x
, который получает значения x
, я могу "адаптировать мой собственный интерфейс", поэтому вместо этого я должен работать с значениями y
(при этом все еще представляя интерфейс "получает x
значения" ) мои абоненты) с помощью функции x -> y
. Обычно мы так не думаем; даже будучи разработчиком f x
, я думаю о том, чтобы адаптировать то, что я вызываю, вместо того, чтобы "адаптировать мой интерфейс вызывающего абонента ко мне". Но это еще одна перспектива, которую вы можете предпринять.
Единственное полу-реальное использование, которое я сделал из Contravariant
(в отличие от неявного использования контравариантности функций в своих аргументах с использованием композиции по-правому, что очень часто) было для тип Serialiser a
, который мог бы сериализовать значения x
. Serialiser
должен был быть Contravariant
, а не a Functor
; учитывая, что я могу сериализовать Foos, я также могу сериализовать Bars, если могу Bar -> Foo
. 4 Но когда вы понимаете, что Serialiser a
в основном a -> ByteString
, это становится очевидным; Я просто повторяю специальный пример примера a -> r
.
В чистом функциональном программировании очень мало пользы от наличия чего-то, что "получает значения", без того, чтобы он тоже возвращал что-то обратно, поэтому все контравариантные функторы имеют тенденцию выглядеть как функции, но почти любая простая структура данных, которая может содержать значения произвольный тип будет ковариантным функтором в параметре этого типа. Вот почему Functor
раненые доброе имя раннее и используется повсеместно (ну, и что Functor
был признан как фундаментальная часть Monad
, которая уже широко использовалась до того, как был определен Functor
как класс в Haskell).
В соответствии с ОО я считаю, что контравариантные функторы могут быть значительно более распространены (но не абстрагированы с помощью единой структуры, такой как Contravariant
), хотя также очень легко иметь изменчивость и побочные эффекты, означающие, что параметризованный тип просто не мог (обычно: ваш стандартный контейнер a
, который является читаемым и записываемым, является как эмиттером, так и приемником a
, а не означает, что он является как ковариантным, так и контравариантным, что не означает его).
1 В экземпляре Functor
каждого отдельного f
говорится, как применять произвольные функции к конкретному виду этого f
, не беспокоясь о конкретных типах f
к; хорошее разделение проблем.
2 Этот функтор также является монадой, эквивалентной монаде Reader
. Я не собираюсь выходить за рамки функторов подробно здесь, но, учитывая остальную часть моего сообщения, очевидным вопросом будет "тип a -> r
также какая-то контравариантная монада в a
then?". Контравариантность не относится к монадам, к сожалению (см. Существуют ли контравариантные монады?), но существует контравариантный аналог Applicative
: https://hackage.haskell.org/package/contravariant-1.4/docs/Data-Functor-Contravariant-Divisible.html
3 Обратите внимание, что мой contramap'
здесь не соответствует фактическому contramap
из Contravariant
, как это реализовано в Haskell; вы не можете сделать a -> r
фактическим экземпляром Contravariant
в коде Haskell просто потому, что a
не является последним параметром типа (->)
. Концептуально он работает отлично, и вы всегда можете использовать оболочку newtype для замены параметров типа и создания экземпляра (контравариант определяет тип Op
для этой цели).
4 По крайней мере, для определения "сериализации", который не обязательно включает возможность восстановления панели позже, поскольку он будет сериализовать a Bar одинаково с Foo, на который он отображен без способ включить любую информацию о том, что такое отображение.