在WriterT中`pass`和`listen`如何工作?

时间:2015-03-17 09:54:00

标签: haskell

下面的代码可能不是一个很好的方法,但它是我设法凑齐的东西。基本上,我运行一系列复杂的任务,在此期间记录了几件事。在每个文件的末尾,我将日志转储到.txt文件中,然后转到循环中的下一批文件。

为实现这一目标,我在listen中使用passWriterT(作为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

开始/结束循环的日志条目最后一起结束。

我对logToFilemapFunction的来电不包含来自loopFunction的日志信息并不完全感到惊讶,因为信息还没有通过绑定传递给它。

但我仍然无法理解passlisten的工作原理。还有我如何解决这个问题(不可否认的是小问题)。

4 个答案:

答案 0 :(得分:2)

我们可以确定listenpass几乎全部来自其类型。我们先从听。

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或将输入函数应用于rs(我们无法使用>>=因为它要求我们已经有m)。我们没有要a返回,因此我们必须将该功能应用于rs。我们只能使用一个rs,这些会传递到结果中。

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。我们可以返回一些内容或将输入函数应用于rs并再次重复操作,这对于“listen ing”来说是无意义的。我们return

listen k r s = k r s >>= \(a, s', w) -> return ...

我们需要aws和其他w。我们只有一个a,无法获得任何其他人。

listen k r s = k r s >>= \(a, s', w) -> return ((a,...),...,...)

我们有3种方法可以获得wmemptyw来自行动结果,或将两个w与{{1}合并}}。返回<>毫无意义;用户可能刚刚使用了mempty。复制用mempty记录的内容与运行两次操作一样多,所以我们返回第一个操作记录的内容

<>

我们有两个listen k r s = k r s >>= \(a, s', w) -> return ((a,w),...,...) es:ss。恢复操作的状态更改对于“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。我们可以返回一些内容或将输入函数应用于mr并再次重复操作,这对于“s ing”来说是无意义的。我们pass

return

我们需要pass k r s = k r s >>= \((a, f), s', w) -> return ... as。我们只有一个w,无法获得任何其他人。

a

我们有两个pass k r s = k r s >>= \((a, f), s', w) -> return (a,...,...) es:ss。恢复操作的状态更改对于“s' ing”无效,因此我们返回已更改的状态pass

s'

我们有4种方法可以获得pass k r s = k r s >>= \((a, f), s', w) -> return (a,s',...) wmempty来自行动结果,将两个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 ...wfw构建的精细结构,但所有这些结构都可以在{ {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块本身完成运行之前,loopFunctionlogToFile的日志不会由do写入。

LoggerT

让我们发明一个名为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 mlift将每个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")