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

Можно ли расширять бесплатные переводчики монады?

Учитывая бесплатную DSL-монаду, такую ​​как:

data FooF x = Foo String x
            | Bar Int    x
  deriving (Functor)

type Foo = Free FooF

И случайный интерпретатор для Foo:

printFoo :: Foo -> IO ()
printFoo (Free (Foo s n)) = print s >> printFoo n
printFoo (Free (Bar i n)) = print i >> printFoo n

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

printFoo' :: Foo -> IO ()
printFoo' (Free (Foo s n)) = print s >> print "extra info" >> printFoo' n
printFoo' (Free (Bar i n)) = print i >> print "extra info" >> printFoo' n

Как это возможно, путем "обертывания" оригинала printFoo?


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

4b9b3361

Ответ 1

Другие ответы пропустили, как просто free делает это!:) В настоящее время у вас есть

{-# LANGUAGE DeriveFunctor #-}

import Control.Monad.Free

data FooF x = Foo String x
            | Bar Int    x
  deriving (Functor)

type Foo = Free FooF

program :: Free FooF ()
program = do
  liftF (Foo "Hello" ())
  liftF (Bar 1 ())
  liftF (Foo "Bye" ())

printFoo :: Foo () -> IO ()
printFoo (Free (Foo s n)) = print s >> printFoo n
printFoo (Free (Bar i n)) = print i >> printFoo n
printFoo (Pure a) = return a

который дает

*Main> printFoo program 
"Hello"
1
"Bye"

Это прекрасно, но iterM может сделать необходимую для вас сантехнику

printFooF :: FooF (IO a) -> IO a
printFooF (Foo s x) = print s >> x
printFooF (Bar i x) = print i >> x

printFooBetter :: Foo () -> IO ()
printFooBetter = iterM printFooF

Тогда получим

*Main> printFooBetter program
"Hello"
1
"Bye"

Хорошо, отлично, как и раньше. Но printFooF дает нам больше гибкость, чтобы увеличить переводчик по линиям, которые вы хотите

printFooFExtra :: FooF (IO a) -> IO a
printFooFExtra = (print "stuff before IO action" >>)
                 . printFooF
                 . fmap (print "stuff after IO action" >>)

printFooExtra :: Foo () -> IO ()
printFooExtra = iterM printFooFExtra

то получим

*Main> printFooExtra program
"stuff before IO action"
"Hello"
"stuff after IO action"
"stuff before IO action"
1
"stuff after IO action"
"stuff before IO action"
"Bye"
"stuff after IO action"

Спасибо Габриэль Гонсалес за популяризацию свободных монадов и Эдварда Кмета для написания библиотеки!:)

Ответ 2

Здесь очень простое решение с использованием пакета operational - разумная альтернатива свободным монадам.

Вы можете просто включить функцию printFoo в часть, которая печатает собственно инструкцию и часть, которая добавляет дополнительную информацию, стандартную обработку дублирования кода, подобную этой.

{-# LANGUAGE GADTs #-}

import Control.Monad.Operational

data FooI a where
    Foo :: String -> FooI ()
    Bar :: Int    -> FooI ()

type Foo = Program FooI

printFoo :: Foo a -> IO a
printFoo = interpretWithMonad printExtra
    where
    printExtra :: FooI a -> IO a
    printExtra instr = do { a <- execFooI instr; print "extra info"; return a; }

execFooI :: FooI a -> IO a
execFooI (Foo s) = print s
execFooI (Bar i) = print i

Ответ 3

Вы ищете что-то вроде этого?

Ваш исходный код будет

{-# LANGUAGE DeriveFunctor #-}

import Control.Monad.Free

data FooF a = Foo String a | Bar Int a deriving (Functor)

type Foo = Free FooF

printFoo :: Show a => Foo a -> IO ()
printFoo (Free (Foo s n)) = print s >> printFoo n
printFoo (Free (Bar i n)) = print i >> printFoo n
printFoo (Pure a)         = print a

Затем вы можете определить простую функцию-обертку и рекурсивный аннотатор, который добавляет дополнительную информацию для каждого уровня Foo (очевидно, это может быть так сложно, как вам нравится).

annotate :: Foo a -> Foo a
annotate (Free (Foo s n)) = wrapper (Free (Foo s (annotate n)))
annotate (Free (Bar i n)) = wrapper (Free (Bar i (annotate n)))
annotate (Pure a)         = wrapper (Pure a)

wrapper :: Foo a -> Foo a
wrapper n = Free (Foo "Extra info" n)

Теперь определите некоторые конструкторы удобства, которые определяют ваш DSL

foo :: String -> a -> Foo a
foo s a = Free (Foo s (Pure a))

bar :: Int -> a -> Foo a
bar i a = Free (Bar i (Pure a))

Это означает, что вы можете создавать объекты Foo a только с помощью интерфейса monad и вашего DSL

example = do
    i <- return 1
    a <- foo "Created A" i
    b <- bar 123 a
    c <- foo "Created C" b
    return c

Теперь, если вы загружаете GHCI, вы можете работать либо с оригинальным example, либо с аннотированной версией

>> printFoo example
"Created A"
123
"Created C"
1
>> printFoo (annotate example)
"Extra info"
"Created A"
"Extra info"
123
"Extra info"
"Created C"
"Extra info"
1

Ответ 4

Обе вещи просто пересекают структуру и накапливают результат индуктивной обработки. Это требует обобщения итерации через катаморфизм.

> newtype Fix f = Fix {unFix :: f (Fix f)}
> data N a b x = Z a | S b x deriving (Functor)
> type Nat a b = Fix (N a b)
> let z = Fix . Z
> let s x = Fix . S x
> let x = s "blah" $ s "doo" $ s "duh" $ z 0
> let annotate (Z x) = s "annotate" $ z x;
      annotate (S x y) = s "annotate" $ s x y
> let exec (Z x) = print x; exec (S x y) = print x >> y
> let cata phi = phi . fmap (cata phi) . unFix
>
> cata exec x
"blah"
"doo"
"duh"
0
>
> cata exec $ cata annotate x
"annotate"
"blah"
"annotate"
"doo"
"annotate"
"duh"
"annotate"
0

Теперь позвольте мне более подробно рассказать, что происходит, поскольку в комментариях были некоторые запросы, и проблема в том, что она больше не будет монадой, если я использую Fix.

Рассмотрим функтор G:

G(X) = A + F(G(X))

Здесь F - произвольный функтор. Тогда для любого А найдется неподвижная точка (F и G, очевидно, многочлены - мы находимся в Hask). Так как мы сопоставляем каждый объект A категории с объектом категории, мы говорим о функторе неподвижных точек T (A). Оказывается, это Монада. Так как это монада для любого функтора F, T (A) является свободной монадой. (Вы увидите, что это, очевидно, Монада из приведенного ниже кода)

{-# LANGUAGE DeriveFunctor
           , TypeSynonymInstances #-}

newtype Fix f = Fix {unFix :: f (Fix f)} -- the type of Fixed point of a functor
newtype Compo f g x = Compo {unCompo :: f (g x)} -- composition of functors

instance (Functor f, Functor g) => Functor (Compo f g) where -- composition of functors is a functor
  fmap f = Compo . fmap (fmap f) . unCompo

data FreeF a x = Pure a | Free x deriving (Functor) -- it is a bi-functor, really;
                -- this derives functor in x

-- a special case of fmap - the fmap with unwrapping; useful to eliminate pattern matching
ffmap :: (a -> b) -> FreeF b a -> b
ffmap f x = case fmap f x of -- unwrapping, since now distinction between Pure and Free is not important
              Pure a -> a
              Free a -> a

-- Free Monad is a functor of fixed points of functor G(X)
-- G(X) = A + F(G(X))
type Free f a = Fix (Compo (FreeF a) f) -- fixed point of composition F . (FreeF a)


-- unfortunately, when defined as type, (Free f a) cannot be declared
-- as a Monad (Free f) - Haskell wants Free f to be with `a`
-- instance Monad (Free f) where -- this derives a functor in a at the same time;
--                          note that fmap will work in x, and is not meant
--                          to be equal to (m >>= return . f), which is in `a`
--   return a = Fix $ Compo $ Pure a
--   (Fix (Compo (Pure a))) >>= f  = f a
--   (Fix (Compo (Free fx))) >>= f = Fix $ Compo $ Free $ fmap (>>= f) fx

ret :: (Functor f) => a -> Free f a -- yet it is a monad: this is return
ret = Fix . Compo . Pure

-- and this is >>= of the monad
bind :: (Functor f) => Free f a -> (a -> Free f b) -> Free f b
bind (Fix (Compo (Pure a))) f = f a
bind (Fix (Compo (Free fx))) f = Fix $ Compo $ Free $ fmap (`bind` f) fx

-- Free is done

-- here is your functor FooF
data FooF x = Z Int x | S String x deriving (Functor)

type Foo x = Free FooF x

-- catamorphism for an algebra phi "folds" any F(X) (represented by fixed point of F)
-- into X
cata :: (Functor f) => (f x -> x) -> Fix f -> x
cata phi = phi . fmap (cata phi) . unFix

-- helper functions to construct "Foo a"
z :: Int -> Foo a -> Foo a
z x = Fix . Compo . Free . Z x

s :: String -> Foo a -> Foo a
s x = Fix . Compo . Free . S x

tip :: a -> Foo a
tip = ret

program :: Foo (IO ())
program = s "blah" $ s "doo" $ s "duh" $ z 0 $ tip $ return ()

-- This is essentially a catamorphism; I only added a bit of unwrapping
cata' :: (Functor f) => (f a -> a) -> Free f a -> a
cata' phi = ffmap (phi . fmap (cata' phi)) . unCompo . unFix

exec (Z x y) = print x >> y
exec (S x y) = print x >> y

annotate (Z x y) = s "annotated Z" $ z x y
annotate (S x y) = s "met S" $ s x y

main = do
         cata' exec program
         cata' exec $ cata' annotate (program `bind` (ret . ret))
           -- cata' annotate (program >>= return . return)
           -- or rather cata' annotate $ fmap return program

program - Foo (IO ()). fmap in a (помните, что FreeF является би-функтором - нам нужен fmap в a), может превратить program в Foo (Foo (IO ())) - теперь катаморфизм для аннотата может построить новый Foo (IO ()).

Обратите внимание, что cata' является iter из Control.Monad.Free.

Ответ 5

Если вы хотите немного изменить оригинальный интерпретатор (изменив способ обработки терминала)

{-# LANGUAGE DeriveFunctor #-}

import Control.Monad.Free
import Control.Monad.Morph
import Pipes

data FooF a = Foo String a | Bar Int a deriving (Functor)

printFoo :: Free FooF a -> IO a
printFoo (Free (Foo s n)) = print s >> printFoo n
printFoo (Free (Bar i n)) = print i >> printFoo n
printFoo (Pure a)         = return a

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

В решении используются пакеты pipes и mmorph.

Сначала вы должны определить своего рода "предварительный перевод", который поднимает свободную монаду в Producer из pipes. Операторы yield () в производитее обозначают точки, в которые добавлено дополнительное действие.

pre :: Free FooF a -> Producer () (Free FooF) a
pre (Free (Foo s n)) = lift (Free . Foo s $ return ()) >> yield () >> pre n
pre (Free (Bar i n)) = lift (Free . Bar i $ return ()) >> yield () >> pre n
pre (Pure a)         = lift . Pure $ a 

(В более сложном примере операторы yield могут содержать дополнительную информацию, такую ​​как сообщения журнала.)

Затем вы пишете функцию, которая применяет интерпретатор printFoo под Producer, используя hoist from mmorph:

printFooUnder :: Producer () (Free FooF) a -> Producer () IO a
printFooUnder = hoist printFoo

Итак, у нас есть функция, которая "интерпретирует" свободную монаду в IO, но в некоторых точках испускает значения (), которые мы должны решить, как обращаться.

Теперь мы можем определить расширенный интерпретатор, который повторно использует старый интерпретатор:

printFooWithReuse :: Show a => Free FooF a -> IO () 
printFooWithReuse foo = do
    finalv <- runEffect $ for (printFooUnder . pre $ foo) 
                              (\_ -> lift (print "extra info"))
    print finalv

После тестирования он работает:

printFooWithReuse $ Free (Foo "nah" (Pure 4))
-- > "nah"
-- > "extra info"
-- > 4

Если вам захочется вставить дополнительные действия вручную, вы можете отказаться от написания "пре-интерпретатора" и работать непосредственно в монаде Producer () (Free FooF).

(Это решение также может быть достигнуто путем разбиения свободного монадного трансформатора вместо Producer. Но я думаю, что использование Producer немного проще.)