Лучше ли определять Фектор в терминах Аппликативного в терминах Монады, или наоборот?
Это общий вопрос, не связанный с какой-либо частью кода.
Скажем, у вас есть тип T a
, которому может быть предоставлен экземпляр Monad
. Поскольку каждая монада является Applicative
, назначая pure = return
и (<*>) = ap
, а затем каждое приложение является Functor
через fmap f x = pure f <*> x
, лучше ли сначала определить ваш экземпляр Monad
, а затем тривиально дать T
экземпляры Applicative
и Functor
?
Мне кажется немного отсталым. Если бы я занимался математикой вместо программирования, я бы подумал, что сначала я покажу, что мой объект является функтором, а затем продолжайте добавлять ограничения, пока я не покажу, что это монада. Я знаю, что Haskell просто вдохновлен Теорией категорий и, очевидно, методы, которые можно использовать при построении доказательства, - это не те методы, которые можно было бы использовать при написании полезной программы, но я хотел бы получить мнение сообщества Haskell. Лучше ли идти от Monad
до Functor
? или от Functor
до Monad
?
Ответы
Ответ 1
Я обычно пишу и вижу сначала написанный экземпляр Functor
. Дважды, потому что, если вы используете прагму LANGUAGE DeriveFunctor
, тогда data Foo a = Foo a deriving ( Functor )
работает большую часть времени.
Сложные биты заключаются в согласовании экземпляров, когда ваш Applicative
может быть более общим, чем ваш Monad
. Например, здесь Err
тип данных
data Err e a = Err [e] | Ok a deriving ( Functor )
instance Applicative (Err e) where
pure = Ok
Err es <*> Err es' = Err (es ++ es')
Err es <*> _ = Err es
_ <*> Err es = Err es
Ok f <*> Ok x = Ok (f x)
instance Monad (Err e) where
return = pure
Err es >>= _ = Err es
Ok a >>= f = f a
Выше я определил экземпляры в Functor
-to- Monad
порядке и, взяв отдельно, каждый экземпляр верен. К сожалению, экземпляры Applicative
и Monad
не выравниваются: ap
и (<*>)
наблюдаются разные, как и (>>)
и (*>)
.
Err "hi" <*> Err "bye" == Err "hibye"
Err "hi" `ap` Err "bye" == Err "hi"
В целях чувствительности, особенно после того, как предложение Аппликативное/Монада находится в руках каждого человека, они должны совпадать. Если вы определили instance Applicative (Err e) where { pure = return; (<*>) = ap }
, то они будут выровнены.
Но тогда, наконец, вы можете тщательно разделить различия в Applicative
и Monad
, чтобы они вел себя по-разному мягкими способами - например, с более ленивым или более эффективным экземпляром Applicative
. Это на самом деле происходит довольно часто, и я чувствую, что жюри все еще немного отличается от того, что означает "доброкачественный" и под каким "наблюдением" должны совпадать ваши экземпляры. Возможно, самое известное использование этого в проекте Haxl в Facebook, где экземпляр Applicative
больше распараллелен, чем экземпляр Monad
, и, следовательно, намного эффективнее за счет некоторых довольно серьезных "ненаблюдаемых" побочных эффектов.
В любом случае, если они отличаются, документируйте его.
Ответ 2
Я часто выбираю обратный подход по сравнению с ответом Абрахамсона. Я вручную определяю только экземпляр Monad
и определяю Applicative
и Functor
в терминах этого с помощью уже определенных функций в Control.Monad
, который делает эти экземпляры одинаковыми для абсолютно любой монады, то есть:
instance Applicative SomeMonad where
pure = return
(<*>) = ap
instance Functore SomeMonad where
fmap = liftM
В то время как определение Functor
и Applicative
всегда "беспроблемное" и очень легко рассуждать, я должен отметить, что это не окончательное решение, поскольку есть случаи, когда случаи могут быть реализованы более эффективно или даже предоставлять новые функции. Например, экземпляр Applicative
Concurrently
выполняет вещи... одновременно, в то время как экземпляр Monad
может выполнять их последовательно только из-за монады природы.
Ответ 3
Functor
экземпляры, как правило, очень просты в определении, я обычно делаю это вручную.
Для Applicative
и Monad
это зависит. pure
и return
обычно аналогичны легко, и на самом деле не имеет значения, в каком классе вы помещаете расширенное определение. Для привязки иногда бывает полезно перейти на "способ категории", т.е. Сначала определить специализированный join' :: (M (M x)) -> M x
, а затем a>>=b = join' $ fmap b a
(который, конечно, не будет работать, если вы определили fmap
в терминах >>=
), Тогда, вероятно, полезно просто повторно использовать (>>=)
для экземпляра Applicative
.
В других случаях экземпляр Applicative
может быть написан довольно легко или более эффективен, чем общая производная от Monad. В этом случае вы определенно определите <*>
отдельно.
Ответ 4
Магия здесь, что Хаскелл использует обозначение Клейсли-типлета монады, что
более удобный способ, если кто-то хочет использовать монады в императивном программировании, как инструменты.
Я задал тот же вопрос, и ответ наступит через некоторое время, если вы увидите определения
Признак, Аппликативный, Монад в haskell вы пропустите одну ссылку, которая является исходным определением монады, которая содержит только операцию соединения, который можно найти на HaskellWiki.
С этой точки зрения вы увидите, как создаются монады-хэскелл-функторы, аппликативные функторы, монады и триплет Клиесли.
Подробное объяснение можно найти здесь: https://github.com/andorp/pearls/blob/master/Monad.hs
И другие с теми же идеями здесь: http://people.inf.elte.hu/pgj/haskell2/jegyzet/08/Monad.hs
Ответ 5
Я думаю, вы неправильно понимаете, как подклассы работают в Haskell. Они не похожи на подклассы OO! Вместо этого ограничение подкласса, например
class Applicative m => Monad m
говорит, что "любой тип с канонической структурой Monad
должен также иметь каноническую структуру Applicative
". Существуют две основные причины, по которым вы должны установить ограничение:
- Структура подкласса индуцирует структуру суперкласса.
- Структура суперкласса является естественным подмножеством структуры подкласса.
Например, рассмотрим:
class Vector v where
(.^) :: Double -> v -> v
(+^) :: v -> v -> v
negateV :: v -> v
class Metric a where
distance :: a -> a -> Double
class (Vector v, Metric v) => Norm v where
norm :: v -> Double
Первое ограничение суперкласса на Norm
возникает потому, что понятие нормированного пространства действительно слабо, если вы не предполагаете также структуру векторного пространства; вторая возникает потому, что (при заданном векторном пространстве) a Norm
индуцирует a Metric
, что можно доказать, заметив, что
instance Metric V where
distance v0 v1 = norm (v0 .^ negateV v1)
является допустимым экземпляром Metric
для любого V
с действительным экземпляром Vector
и действительной функцией Norm
. Мы говорим, что норма индуцирует метрику. См. http://en.wikipedia.org/wiki/Normed_vector_space#Topological_structure.
Суперклассы Functor
и Applicative
на Monad
похожи на Metric
, а не как Vector
: функции return
и >>=
из Monad
вызывают Functor
и Applicative
структуры:
-
fmap
: может быть определен как fmap f a = a >>= return . f
, который был liftM
в стандартной библиотеке Haskell 98.
-
pure
: это та же операция, что и return
; два имени - это наследие, с которого Applicative
не был суперклассом Monad
.
-
<*>
: может быть определен как af <*> ax = af >>= \ f -> ax >>= \ x -> return (f x)
, который был liftM2 ($)
в стандартной библиотеке Haskell 98.
-
join
: может быть определено как join aa = aa >>= id
.
Таким образом, совершенно разумно, математически, определять операции Functor
и Applicative
в терминах Monad
.