Подтвердить что ты не робот

Monad Stack Penetration Классы с Free/Operational Monad Transformers?

Может ли быть 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 стоит. Однако это была не большая проблема, потому что люди будут в основном использовать предопределенные монады и редко создавать свои собственные. История до сих пор я понимаю, а также подробно, например. в следующих Q & A:

Избежать подъема с помощью трансформаторов Monad

Тогда моя проблема связана с новыми Free monads 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 не поддерживает monad-классы, поэтому я сделал еще одну реализацию 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

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

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

4b9b3361

Ответ 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 из free, а затем создать аналогичную функцию объединения

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

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