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

Избегайте подъема с помощью Monad Transformers

Тогда моя проблема связана с новыми бесплатными монадами 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

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

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

22
nushio 30 Июл 2013 в 05:24

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

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

6
Petr 2 Авг 2013 в 15:22
Спасибо, Петр. С вашей помощью я понял, как объединить два конструктора типов с помощью 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/ master / operating / exe-src /…
 – 
nushio
7 Авг 2013 в 10:08