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

Может ли существовать 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 тип (Operational даже не требует 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

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

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


person nushio    schedule 30.07.2013    source источник


Ответы (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

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

person Petr    schedule 02.08.2013
comment
Спасибо, Петр. С вашей помощью я понял, как объединить два конструктора типов с помощью Either over (* -> *). github.com/nushio3/practice/blob/ master / operating / exe-src / Так же просто написать составные интерпретаторы: github.com/nushio3/practice/blob/master/operational/exe-src/ Мы даже можем составить более двух языков за OverlappingInstances. github.com/nushio3/practice/blob/ мастер / рабочий / exe-src / - person nushio; 07.08.2013