Классы проникновения монадных стеков со свободными / оперативными монадными трансформаторами?

Может ли быть mtl-подобный механизм для монадных преобразователей, созданных FreeT / ProgramT?

Мое понимание истории заключается в следующем. Когда-то был изобретен монадный трансформатор. Тогда люди начали укладывать монадные трансформаторы друг на друга, а потом вставали lift везде. Затем пара человек изобрела классы монад, чтобы мы могли, например, ask :: m r в любой монаде m такой, что MonadReader r m, Это стало возможным благодаря тому, что каждый класс монад проникал в каждый монадный трансформатор, как

(Monoid w, MonadState s m) => MonadState s (WriterT w m)
MonadWriter w m => MonadWriter w (StateT s m)

вам нужна такая пара объявлений экземпляров для каждой пары монадных преобразователей, поэтому, когда есть n монадных преобразователей, n^ 2 стоит. Однако это не было большой проблемой, потому что люди в основном будут использовать предопределенные монады и редко будут создавать свои собственные. Насколько я понимаю, эта история подробно описана, например, в следующих вопросах и ответах:

Избегать подъема с помощью монадных трансформаторов

Тогда моя проблема с новыми бесплатными монадами http://hackage.haskell.org/package/free и операционными монадами http://hackage.haskell.org/package/operational. Они позволяют нам писать наш собственный DSL и использовать его как монады, просто определяя язык как некоторый алгебраический data тип (операционный даже не нужен Functor экземпляры). Хорошей новостью является то, что у нас могут быть монады и монадные трансформаторы бесплатно; тогда как насчет классов монада? Плохая новость заключается в том, что предположение "мы редко определяем наши собственные монадные трансформаторы" больше не выполняется.

В попытке понять эту проблему я сделал два ProgramTи заставил их проникать друг в друга;

https://github.com/nushio3/practice/blob/master/operational/exe-src/test-05.hs

operational Пакет не поддерживает классы монад, поэтому я взял другую реализацию minioperational и модифицировал его так, чтобы он работал так, как мне нужно; https://github.com/nushio3/minioperational

Тем не менее, мне нужно было объявление специализированного экземпляра

instance (Monad m, Operational ILang m) => Operational ILang (ProgramT SLang m) where

потому что общее объявление следующей формы приводит к неразрешимым случаям.

instance (Monad m, Operational f m) => Operational f (ProgramT g m) where

Мой вопрос заключается в том, как мы можем сделать так, чтобы наши Операционные монады проникали друг в друга. Или мое желание иметь проникновение для любой операционной монады некорректно.

Я также хотел бы знать правильный технический термин для проникновения:)

1 ответ

Решение

Я попробовал немного другой подход, который дает хотя бы частичный ответ. Поскольку сложение монад иногда может быть проблематичным, и мы знаем, что все наши монады построены из некоторого типа данных, я вместо этого попытался объединить типы данных.

Я чувствую себя более комфортно с MonadFree поэтому я использовал его, но я полагаю, что аналогичный подход может быть использован для Operational также.

Давайте начнем с определения наших типов данных:

{-# LANGUAGE DeriveFunctor, FlexibleContexts,
             FlexibleInstances, FunctionalDependencies #-}
import Control.Monad
import Control.Monad.Free

data SLang x = ReadStr (String -> x) | WriteStr String x
  deriving Functor
data ILang x = ReadInt (Int -> x) | WriteInt Int x
  deriving Functor

Чтобы объединить два функтора вместе для использования их в свободной монаде, давайте определим их совместный продукт:

data EitherF f g a = LeftF (f a) | RightF (g a)
  deriving Functor

Если мы создадим свободную монаду над EitherF f gмы можем вызывать команды от них обоих. Чтобы сделать этот процесс прозрачным, мы можем использовать MPTC, чтобы разрешить преобразование каждого функтора в целевой:

class Lift f g where
    lift :: f a -> g a
instance Lift f f where
    lift = id

instance Lift f (EitherF f g) where
    lift = LeftF
instance Lift g (EitherF f g) where
    lift = RightF

теперь мы можем просто позвонить lift и преобразовать любую часть в побочный продукт.

С функцией помощника

wrapLift :: (Functor g, Lift g f, MonadFree f m) => g a -> m a
wrapLift = wrap . lift . fmap return

наконец, мы можем создать универсальные функции, которые позволяют нам вызывать команды из всего, что мы можем поднять в функтор:

readStr :: (Lift SLang f, MonadFree f m) => m String
readStr = wrapLift $ ReadStr id

writeStr :: (Lift SLang f, MonadFree f m) => String -> m ()
writeStr x = wrapLift $ WriteStr x ()

readInt :: (Lift ILang f, MonadFree f m) => m Int
readInt = wrapLift $ ReadInt id

writeInt :: (Lift ILang f, MonadFree f m) => Int -> m ()
writeInt x = wrapLift $ WriteInt x ()

Тогда программа может быть выражена как

myProgram :: (Lift ILang f, Lift SLang f, MonadFree f m) => m ()
myProgram = do
  str <- readStr
  writeStr "Length of that str is"
  writeInt $ length str
  n <- readInt
  writeStr "you wanna have it n times; here we go:"
  writeStr $ replicate n 'H'

без определения каких-либо дальнейших случаев.


Хотя все вышеперечисленное работает хорошо, проблема в том, как в общем случае запускать такие составные бесплатные монады. Я не знаю, возможно ли вообще иметь полностью универсальное, сочетаемое решение.

Если у нас есть только один базовый функтор, мы можем запустить его как

runSLang :: Free SLang x -> String -> (String, x)
runSLang = f
  where
    f (Pure x)              s  = (s, x)
    f (Free (ReadStr g))    s  = f (g s) s
    f (Free (WriteStr s' x)) _ = f x s'

Если у нас их два, нам нужно передать состояние обоих из них:

runBoth :: Free (EitherF SLang ILang) a -> String -> Int -> ((String, Int), a)
runBoth = f
  where
    f (Pure x)                       s i  = ((s, i), x)
    f (Free (LeftF  (ReadStr g)))     s i = f (g s) s i
    f (Free (LeftF  (WriteStr s' x))) _ i = f x s' i
    f (Free (RightF (ReadInt g)))     s i = f (g i) s i
    f (Free (RightF (WriteInt i' x))) s _ = f x s i'

Я полагаю, что одна из возможностей состоит в том, чтобы iter :: Functor f => (f a -> a) -> Free f a -> a от свободного, а затем создать похожую, комбинирующую функцию

iter2 :: (Functor f, Functor g)
      => (f a -> a) -> (g a -> a) -> Free (EitherF f g) a -> a

Но у меня не было времени, чтобы попробовать это.

Другие вопросы по тегам