Скажите, что у меня есть два класса типа, определенные следующим образом, которые идентичны в функции, но отличаются на имена:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
class PhantomMonad p where
pbind :: p a -> (a -> p b) -> p b
preturn :: a -> p a
Существует ли способ связать эти два класса так что-то, что является экземпляром PhantomMonad, автоматически будет экземпляр Монады, или будете, экземпляры для каждого класса должны быть явно записаны? Любое понимание больше всего ценилось бы, Спасибо!
Хороший ответ: Нет, то, что вы надеетесь сделать, на самом деле не жизнеспособно. Вы можете написать экземпляр, который выглядит так, как будто он делает то, что вы хотите, возможно, требуя некоторых расширений GHC в процессе, но он не будет работать так, как вам хотелось бы.
Неразумный ответ: Вы, вероятно, сможете добиться желаемого, используя устрашающее метапрограммирование на уровне типов, но это может стать сложным. Это действительно не рекомендуется, если вам не абсолютно это нужно для работы по какой-то причине.
Официально экземпляры не могут действительно зависеть от других экземпляров, потому что GHC смотрит только на «заголовок экземпляра» при принятии решений, а ограничения класса находятся в «контексте». Чтобы создать здесь что-то вроде «синонима класса типа», вам нужно написать то, что выглядит как экземпляр Монады
для всех возможных типов , что, очевидно, не имеет смысла. Вы будете перекрываться с другими экземплярами Монады
, у которых есть свои проблемы.
Вдобавок ко всему, я не думаю, что такой экземпляр будет удовлетворять требованиям проверки завершения для разрешения экземпляра, поэтому вам также понадобится расширение UndecidableInstances
, что означает возможность записи экземпляров который отправит средство проверки типов GHC в бесконечный цикл.
Если вы действительно хотите спуститься в эту кроличью нору, поищите немного на сайте Олега Киселева ; он своего рода покровитель метапрограммирования на уровне типов в Haskell.
Это, конечно, забавный материал, но если вы просто хотите написать код и заставить его работать, вероятно, не стоит тратить силы.
Изменить: Хорошо, задним числом я переоценил проблему здесь. Что-то вроде PhantomMonad
отлично работает как одноразовый и должен делать то, что вы хотите, учитывая перекрывающиеся
- и UndecidableInstances
расширения GHC. Сложный материал начинается, когда вы хотите сделать что-то гораздо более сложное, чем то, о чем идет речь. Я искренне благодарен Норману Рэмси за то, что позвал меня на это - я действительно должен был знать лучше.
Я до сих пор не рекомендую делать такие вещи без уважительной причины, но это не так плохо, как я сказал. Моя вина.
Хотя это и не имеет смысла, попробуйте
instance Monad m => PhantomMonad m where
pbind = (>>=)
preturn = return
(возможно, с отключенными предупреждениями компилятора).
Необычный дизайн. Разве нельзя просто удалить PhantomMonad, поскольку он изоморфен другому классу.
Есть ли способ связать эти два класса вместе, чтобы нечто, являющееся экземпляром PhantomMonad, автоматически стало экземпляром Monad?
Да, но для этого требуются слегка тревожные языковые расширения FlexibleInstances
и UndecidableInstances
:
instance (PhantomMonad m) => Monad m where
return = preturn
(>>=) = pbind
FlexibleInstances
не так уж и плох, но риск неразрешимости немного более тревожный. Проблема в том, что в правиле вывода ничего не становится меньше, поэтому, если вы объедините это объявление экземпляра с другим аналогичным (например, в обратном направлении), вы можете легко заставить средство проверки типов работать бесконечно.
Обычно мне удобно использовать FlexibleInstances
, но я стараюсь избегать UndecidableInstances
без веской причины. Здесь я согласен с предложением Дона Стюарта, что для начала вам лучше использовать Монаду
. Но ваш вопрос больше похож на мысленный эксперимент, ответ таков: вы можете делать все, что хотите, не впадая в пугающий уровень Олега.
Другим решением является использование newtype
. Это не совсем то, что вам нужно, но часто используется в подобных случаях.
Это позволяет связать различные способы указания одной и той же структуры. Например, ArrowApply
(из Control.Arrow) и Monad
эквивалентны. Вы можете использовать Kleisli
, чтобы сделать ArrowApply из монады, и ArrowMonad
, чтобы сделать монаду из ArrowApply.
Также возможны односторонние обертки: WrapMonad
(в Control.Applicative) формирует аппликатив из монады.
class PhantomMonad p where
pbind :: p a -> (a -> p b) -> p b
preturn :: a -> p a
newtype WrapPhantom m a = WrapPhantom { unWrapPhantom :: m a }
newtype WrapReal m a = WrapReal { unWrapReal :: m a }
instance Monad m => PhantomMonad (WrapPhantom m) where
pbind (WrapPhantom x) f = WrapPhantom (x >>= (unWrapPhantom . f))
preturn = WrapPhantom . return
instance PhantomMonad m => Monad (WrapReal m) where
WrapReal x >>= f = WrapReal (x `pbind` (unWrapReal . f))
return = WrapReal . preturn