如果您愿意稍微修改原始解释器(通过更改终端案例的处理方式)
{-# 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
包。
首先,您必须定义一种“preinterpeter”,将 free monad 提升为Producer
from 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
应用解释器,使用from :Producer
hoist
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)
monad 中工作。
(这个解决方案也可以通过分层一个免费的单子变压器而不是 a 来实现Producer
。但我认为使用 aProducer
更容易一些。)