下面的代码可能不是一个很好的方法,但它是我设法凑齐的东西。基本上,我运行一系列复杂的任务,在此期间记录了几件事。在每个文件的末尾,我将日志转储到.txt文件中,然后转到循环中的下一批文件。
为实现这一目标,我在listen
中使用pass
和WriterT
(作为RWST
的一部分)。代码如下:
-- Miscelaneous stuff
newtype Log = Log [String]
type ConnectT a = EitherT String (RWST ConnectReader Log ConnectState IO) a
timeStampLog :: String -> Log
timeStampLog msg = do
theTime <- liftIO $ fmap zonedTimeToLocalTime getZonedTime
let msgStart = show theTime ++ ": "
tell $ Log [msgStart ++ msg]
logToFileIO :: Log -> IO ()
logToFileIO (Log xs) = appendFile "Log.txt" $ "\r\n" ++ intercalate "\r\n" (reverse xs)
---------------------
logToFile :: ConnectT a -> ConnectT ()
logToFile cta = let ctaw = listen cta
in pass $ do
(_,w) <- ctaw
liftIO $ logToFileIO w
return ((),const mempty)
mapFunction :: (Show a) => a -> ConnectT ()
mapFunction a = logToFile $ do
timeStampLog $ "Starting sequence for " ++ show a
lotsOfLogging a
timeStampLog $ "Finishing sequence for " ++ show a
loopFunction :: ConnectT ()
loopFunction = logToFile $ do
timeStampLog "Starting Loop"
mapM_ mapFunction someList
timeStampLog "Finishing Loop"
我最终得到的是这样的:
2015-03-17 20:21:40.8198823: Starting sequence for a
2015-03-17 20:21:41.8198823: (logs for a)
2015-03-17 20:21:41.8198823: Finishing sequence for a
2015-03-17 20:21:41.8198823: Starting sequence for b
2015-03-17 20:21:42.8198823: (logs for b)
2015-03-17 20:21:42.8198823: Finishing sequence for b
2015-03-17 20:21:39.8198823: Starting Loop
2015-03-17 20:21:42.8198823: Finishing Loop
开始/结束循环的日志条目最后一起结束。
我对logToFile
中mapFunction
的来电不包含来自loopFunction
的日志信息并不完全感到惊讶,因为信息还没有通过绑定传递给它。
但我仍然无法理解pass
和listen
的工作原理。还有我如何解决这个问题(不可否认的是小问题)。
答案 0 :(得分:2)
我们可以确定listen
和pass
几乎全部来自其类型。我们先从听。
listen :: (Monoid w, Monad m) => RWST r w s m a -> RWST r w s m (a, w)
展开我们的RWST
listen :: (Monoid w, Monad m) => (r -> s -> m (a, s, w)) -> r -> s -> m ((a, w), s, w)
需要返回m ...
。我们必须提出m
的唯一方法是return
或将输入函数应用于r
和s
(我们无法使用>>=
因为它要求我们已经有m
)。我们没有要a
返回,因此我们必须将该功能应用于r
和s
。我们只能使用一个r
和s
,这些会传递到结果中。
listen k r s = ... (k r s)
现在我们有m (a, s, w)
但需要m ((a, w), s, w)
。我们可以再次运行该操作以获取另一个m
(“listen
ing”的废话),或使用(a, s, w)
m
内的>>=
执行某些操作。
listen k r s = k r s >>= \(a, s' w) -> ...
要使用bind
,我们需要m
。我们可以返回一些内容或将输入函数应用于r
和s
并再次重复操作,这对于“listen
ing”来说是无意义的。我们return
。
listen k r s = k r s >>= \(a, s', w) -> return ...
我们需要a
,w
,s
和其他w
。我们只有一个a
,无法获得任何其他人。
listen k r s = k r s >>= \(a, s', w) -> return ((a,...),...,...)
我们有3种方法可以获得w
:mempty
,w
来自行动结果,或将两个w
与{{1}合并}}。返回<>
毫无意义;用户可能刚刚使用了mempty
。复制用mempty
记录的内容与运行两次操作一样多,所以我们返回第一个操作记录的内容。
<>
我们有两个listen k r s = k r s >>= \(a, s', w) -> return ((a,w),...,...)
es:s
和s
。恢复操作的状态更改对于“s'
ing”无效,因此我们返回已更改的状态listen
。
s'
现在我们面临着唯一有趣的选择:我们应该为记录的内容保留listen k r s = k r s >>= \(a, s', w) -> return ((a,w),s',...)
?对于记录的内容,用户具有“w
ed”;我们现在可以说这是他们的问题并将日志重置为listen
。但是“mempty
ing”并不表示它应该改变某些东西的作用,它只应该观察它。因此,我们保持生成的日志listen
完整无缺。
w
如果我们再次将其包含在listen k r s = k r s >>= \(a, s', w) -> return ((a,w),s',w)
s中,我们有
RWST
我们所做的只是运行输入操作,并将结果中包含的内容与结果listen m = RWST \r s -> (runRWST m) r s >>= \(a, s', w) -> return ((a,w),s',w)
包含在一起作为元组。这符合documentation for listen
:
a
是一个执行操作listen m
并将其输出添加到计算值的操作。m
runRWST (listen m) r s = liftM (\ (a, w) -> ((a, w), w)) (runRWST m r s)
我们像以前一样开始,展开pass :: (Monoid w, Monad m) => RWST r w s m (a, w -> w) -> RWST r w s m a
RWST
我们按照与pass :: (Monoid w, Monad m) => (r -> s -> m ((a, w->w), s, w)) -> r -> s -> m (a, s, w)
m
listen
现在我们有pass k r s = ... (k r s)
但需要m ((a, w->w), s, w))
。我们可以再次运行该操作以获取另一个m (a, s, w)
(“m
ing”的废话),或使用pass
((a, w->w), s, w)
内的m
执行某些操作。
>>=
要使用pass k r s = k r s >>= \((a, f), s', w) -> ...
,我们需要bind
。我们可以返回一些内容或将输入函数应用于m
和r
并再次重复操作,这对于“s
ing”来说是无意义的。我们pass
。
return
我们需要pass k r s = k r s >>= \((a, f), s', w) -> return ...
,a
和s
。我们只有一个w
,无法获得任何其他人。
a
我们有两个pass k r s = k r s >>= \((a, f), s', w) -> return (a,...,...)
es:s
和s
。恢复操作的状态更改对于“s'
ing”无效,因此我们返回已更改的状态pass
。
s'
我们有4种方法可以获得pass k r s = k r s >>= \((a, f), s', w) -> return (a,s',...)
:w
,mempty
来自行动结果,将两个w
与{{1}组合在一起},或将函数w
应用于另一个<>
。将结果设置为f
会让我们想知道用户为何提供了函数w
。他们自己。复制使用mempty
记录的内容与运行两次操作一样多无意义。我们应该将函数f :: w -> w
应用于某些东西。
<>
我们可以将f
应用于从pass k r s = k r s >>= \((a, f), s', w) -> return (a,s',f ...)
和f
构建的内容,但如果是这种情况,则所有mempty
都等同于{{1} }};它的类型也可能是<>
。我们可以将f
应用于从const ...
,w
,f
和w
构建的精细结构,但所有这些结构都可以在{ {1}}如果我们只是传递它mempty
。
<>
如果我们再次将其包含在f
s中,我们有
f
我们运行了输入操作并更改了该操作导致的函数所记录的内容。这符合documentation for pass
:
w
是一个执行动作pass k r s = k r s >>= \((a, f), s', w) -> return (a,s',f w)
的动作,它返回一个值和一个函数,并返回该值,将该函数应用于输出。RWST
答案 1 :(得分:1)
现有WriterT w m
无法在基础m
中执行任何操作,直到操作完成并且w
已组合完成后才执行记录。正如你的问题所示,这令人困惑。在do
块本身完成运行之前,loopFunction
块logToFile
的日志不会由do
写入。
让我们发明一个名为WriterT
的新LoggerT
。我们的新LoggerT
将提供新功能
logTells :: (Monoid w, Monoid w', Monad m) =>
(w -> LoggerT w' m ()) -> LoggerT w m a -> LoggerT w' m a
这背后的直觉是:我们能够提供一个动作(类型为w -> LoggerT w' m ()
)来记录每个tell
,用记录的结果替换动作的结果。如果我们将用户tell
与我们<>
一起粉碎两件事,我们将无法再记录这两件事;我们只能记录<>
的结果。由于我们的LoggerT
永远无法使用<>
,因此永远不会需要Monoid
个实例。我们必须从Monoid
中的所有内容中删除LoggerT
约束。
logTells :: (Monad m) =>
(w -> LoggerT w' m ()) -> LoggerT w m a -> LoggerT w' m a
我们需要记住每个tell
,以便我们以后可以替换它。但是当我们更换它&#34;之后&#34;时,日志记录应该在tell
出现在代码中的那一刻发生。例如,如果我们做
processX :: LoggerT String m ()
processX = do
tell "Starting process X"
lotsOfProcessing
tell "Finishing process X"
然后&#34;之后&#34;写logTells logToFile processX
我们希望得到的结果如下所示。
logTells logToFile processX = do
logToFile "Starting process X"
lotsOfProcessing
logToFile "Finishing process X"
在lotsOfProcessing
logToFile
tell "Starting process X"
已经发生之前,tell
都不会发生。这意味着当用户data
给我们提供一些东西时,我们不仅需要记住我们被告知的内容,还要记住之后发生的一切。我们记得&#34; data LoggerT w m a
= Tell w (LoggerT w m a)
| ...
tell :: w -> LoggerT w m ()
tell w = Tell w (return ())
的构造函数中的内容。
Monad
我们还需要能够在基础Lift (m a)
中执行操作。添加另一个构造函数LoggerT w m a
会很诱人,但是我们无法决定基础计算的结果。相反,我们会让它决定要运行的整个未来data LoggerT w m a
= Tell w (LoggerT w m a)
| M (m (LoggerT w m a))
...
。
lift
如果我们尝试m a
基础计算LoggerT
进入a
,我们现在遇到了问题;我们无法将LoggerT w m a
转换为M
以将其置于instance MonadTrans (LoggerT w m) where
lift ma = M (??? ma)
构造函数中。
lift
我们可以尝试return
来自基础Monad
的{{1}},但这只是一个循环定义。我们将为Return
添加另一个构造函数。
data LoggerT w m a
= Tell w (LoggerT w m a)
| M (m (LoggerT w m a))
| Return a
instance MonadTrans (LoggerT w m) where
lift = M . liftM Return
要完成我们的monad转换器,我们将编写一个Monad
实例。
instance Monad m => Monad (LoggerT w m) where
return = Return
la0 >>= k = go la0
where
go (Tell w la ) = Tell w (go la)
go (M mla) = M (liftM go mla)
go (Return a ) = Return a
我们现在可以定义logTells
。它将每个Tell
替换为要执行的操作以记录它。
logTells :: (w -> LoggerT w' m ()) -> LoggerT w m a -> LoggerT w' m a
logTells k = go
where
go (Tell w la ) = k w >> go la
go (M mla) = M (liftM go mla)
go (Return a) = return a
最后,我们提供了一种退出LoggerT
的方法,方法是将所有Tell
替换为一个与logTells
非常相似但删除{{LoggerT
的动作。 1}}来自结果。
因为它将摆脱LoggerT
我们称之为runLoggerT
并交换参数以匹配其他变换器的约定。
runLoggerT :: LoggerT w m a -> (w -> m ()) -> m a
runLoggerT la0 k = go la0
where
go (Tell w la ) = k w >> go la
go (M mla) = liftM go mla
go (Return a) = return a
LoggerT
已经存在,我们不需要自己编写。它来自非常成熟的Producer
库中的pipes。
管道库中的Producer
是正确的日志变换器。
type Producer b = Proxy X () () b
每个Proxy
都有MonadTrans (Proxy a' a b' b)
个实例和Monad m => Monad (Proxy a' a b' b m)
个实例。
我们tell
使用yield
登录的内容。
yield :: Monad m => a -> Producer' a m ()
tell = yield
当我们知道我们想要对yield
做什么时,我们会使用for
替换它们。
for :: Monad m =>
Proxy x' x b' b m a' ->
(b -> Proxy x' x c' c m b')
-> Proxy x' x c' c m a'
专门针对Producer
和()
,for
的类型为
for :: Monad m =>
Producer b m a ->
(b -> Producer c m ()) ->
Producer c m a
logTells = flip for
如果我们使用基础monad中的操作替换每个yield
,我们就不会再生成任何内容了,可以使用runEffect
运行Proxy
。< / p>
runEffect :: Monad m => Effect m r -> m r
runEffect :: Monad m => Proxy X () () X m r -> m r
runEffect :: Monad m => Producer X m r -> m r
runLoggerT la0 k = runEffect $ for la0 (lift . k)
我们甚至可以使用hoist
恢复WriterT
,替换基础monad(每个Proxy a' a b' b
都有MFunctor
个实例)。
hoist :: (Monad m, MFunctor t) => (forall a. m a -> n a) -> t m b -> t n b
我们使用hoist
将基础monad替换为WriterT w m
,lift
将每个m a
替换为WriterT w m a
。然后我们将yield
替换为lift . tell
,然后运行结果。
toWriterT :: (Monad m, Monoid w) => Producer w m r -> WriterT w m r
toWriterT p0 = runEffect $ for (hoist lift p0) (lift . tell)
toWriterT p0 = runLoggerT (hoist lift p0) tell
Producer
基本上是免费WriterT
,对于正在撰写的项目不需要Monoid
。
答案 2 :(得分:1)
这是一个使用censor
的简化但绝对真实的示例(根据pass
定义为
censor :: (MonadWriter w m) => (w -> w) -> m a -> m a
censor f m = pass $ (,f) <$> m
)收集lambda术语的自由变量:
import Control.Monad.Writer
import Data.Set (Set)
import qualified Data.Set as Set
type VarId = String
data Term = Var VarId
| Lam VarId Term
| App Term Term
freeVars :: Term -> Set VarId
freeVars = execWriter . go
where
go :: Term -> Writer (Set VarId) ()
go (Var x) = tell $ Set.singleton x
go (App f e) = go f >> go e
go (Lam x e) = censor (Set.delete x) $ go e
现在,你可以在没有所有Writer
机制的情况下实现这一点,但请记住这只是一个简单的例子,它代表了一些更复杂的编译/分析功能,其中跟踪自由变量只是其中之一继续。
答案 3 :(得分:0)
文档足够清晰? http://hackage.haskell.org/package/mtl-2.2.1/docs/Control-Monad-Writer-Lazy.html#g:1
示例(在ghci中运行以下内容)
import Control.Monad.Writer
runWriterT ( do (a,w) <- listen $ do { tell "foo" ; return 42 } ; tell $ reverse w ; return a )
==> (42,"foooof")
runWriterT ( pass $ do { tell "foo" ; return (42,reverse) } )
==> (42,"oof")