如何编写一个在执行 monad 中的每个语句时打印"step i of N"的 monad?



我甚至不确定这在任何类型的monad中都是可能的;它违反了莫纳德定律吗?但它似乎在某种结构中是可能的。具体来说,有没有什么方法可以让我写一些像这样的东西

do
  someOp ()
  someOtherOp ()
  thirdOp ()

它会打印

step 1 of 3
step 2 of 3
step 3 of 3

这需要TemplateHaskell吗?或者monad可以工作吗?(如果需要TemplateHaskell,如何做到这一点?)

我认为您希望步骤自动显示,而不必在代码中添加日志语句。

使用monad这样做的问题是,它们灵活:在任何时候,其余计算的"形状"都可以取决于计算过程中获得的值。这在(>>=)的类型中是明确的,即m a -> (a -> m b) -> m b

因此,在运行计算之前,您无法知道固定数量的总步骤N

然而,Haskell提供了另外两个抽象,它们用monad的一些能力和灵活性换取了提前执行更多"静态"分析的机会:应用函子和箭头。

应用函子虽然非常有用,但对于你的需求来说可能太"弱"了。您不能在应用函子中编写函数,当应用于值时,该函数会将该值打印到控制台。这在"习语是不经意的,箭头是细致的,单子是混杂的"一文中得到了解释,该文包含了一些关于每种抽象极限的启发性例子(在该文中,应用函子被称为"习语")

箭头在表达能力和静态分析的适应性之间提供了更好的折衷。箭头计算的"形状"在静态管道中是固定的。计算过程中获得的数据可能会影响管道中稍后的效果(例如,您可以打印计算中先前效果获得的值),但不会更改管道的形状或步骤数。

因此,如果你可以使用Kleisli箭头(monad的箭头)来表达你的计算,也许你可以编写某种箭头转换器(而不是monad转换器),它增加了自动日志记录功能。

箭头包提供了许多箭头转换器。我认为StaticArrow可以用来自动跟踪总步数。但是,您仍然需要编写一些功能来实际发出消息。

编辑:下面是一个如何使用箭头计算步骤数的示例:

module Main where
import Data.Monoid
import Control.Monad
import Control.Applicative
import Control.Arrow
import Control.Arrow.Transformer
import Control.Arrow.Transformer.Static
type SteppedIO a b = StaticArrow ((,) (Sum Int)) (Kleisli IO) a b
step :: (a -> IO b) -> SteppedIO a b
step cmd = wrap (Sum 1, Kleisli cmd)
countSteps :: SteppedIO a b -> Int
countSteps = getSum . fst . unwrap
exec :: SteppedIO a b -> a -> IO b
exec =  runKleisli . snd . unwrap 
program :: SteppedIO () ()
program =
    step (_ -> putStrLn "What is your name?")  
    >>>
    step (_ -> getLine)
    >>>
    step (putStrLn . mappend "Hello, ")
main :: IO ()
main = do
    putStrLn $ "Number of steps: " ++ show (countSteps program)
    exec program ()

请注意,步骤3的效果受到步骤2中产生的值的影响。使用应用程序无法做到这一点。

我们确实使用了StaticArrow所需的(,) (Sum Int)应用程序来编码静态信息(此处仅为步骤数)。

在执行步骤时显示这些步骤将需要更多的工作。

编辑#2如果我们处理的命令序列中没有任何效果取决于前一个效果产生的值,那么我们可以避免使用箭头,只使用应用函子来计算步骤:

module Main where
import Data.Monoid
import Control.Applicative
import Data.Functor.Compose
type SteppedIO a = Compose ((,) (Sum Int)) IO a
step :: IO a -> SteppedIO a
step cmd = Compose (Sum 1, cmd)
countSteps :: SteppedIO a -> Int
countSteps = getSum . fst . getCompose
exec :: SteppedIO a -> IO a
exec =  snd . getCompose
program :: SteppedIO () 
program =
    step (putStrLn "aaa") 
    *>  
    step (putStrLn "bbb")
    *>
    step (putStrLn "ccc")
main :: IO ()
main = do
    putStrLn $ "Number of steps: " ++ show (countSteps program)
    exec program 

Data.Functor.Compose来自transformers封装。

编辑#3以下代码扩展了以前的Applicative步数计数解决方案,使用pipes包实际发出通知。基于箭头的解决方案可以以类似的方式进行调整。

module Main where
import Data.Monoid
import Control.Applicative
import Control.Monad.State
import Data.Functor.Compose
import Pipes
import Pipes.Lift
type SteppedIO a = Compose ((,) (Sum Int)) (Producer () IO) a
step :: IO a -> SteppedIO a
step cmd = Compose (Sum 1, yield () *> lift cmd)
countSteps :: SteppedIO a -> Int
countSteps = getSum . fst . getCompose
exec :: SteppedIO a -> Producer () IO a
exec =  snd . getCompose
stepper :: MonadIO m => Int -> Consumer () m a
stepper n = evalStateP 0 $ forever $ do 
    await
    lift $ modify succ
    current <- lift get
    liftIO $ putStrLn $ "step " ++ show current ++ " of " ++ show n
program :: SteppedIO () 
program = *** does not change relative to the previous example ***
main :: IO ()
main = runEffect $ exec program >-> stepper (countSteps program)

虽然我认为Daniel Díaz的箭头解决方案是实现这一点的完美方法,但肯定有一个更简单的解决方案(我刚刚看到,他也在注释中指出了这一点),就像在您的示例中一样,不同的函数调用之间不会传递任何数据。

请记住,由于Haskell是懒惰的,函数可以做很多需要其他语言中的宏的事情。特别是,有一个IO操作的列表是没有任何问题的。(也绝对安全:由于纯粹性,这些在Haskell中不可能"提前失效"!)然后,您可以简单地将此列表的长度作为总计数,将其与打印语句交错,然后就可以完成了。所有核心语言,不需要TH!

sequenceWithStepCount :: [IO()] -> IO()
sequenceWithStepCount actions = go actions 0
 where nTot = length actions
       go [] _ = putStrLn "Done!"
       go (act:remains) n = do
             putStrLn ("Step "++show n++" of "++show nTot)
             act
             go remains $ succ n

像一样使用

do
 sequenceWithStepCount [
     someOp ()
   , someOtherOp ()
   , thirdOp ()
   ]

根据您的意思,有两种方式可能违反法律。

例如,如果return被算作一个步骤,那么你就会违反第一个monad定律:

do x <- return  /=  f x
   f x

类似地,如果将两个步骤抽象到另一个命名函数中算作删除一个步骤,那么您也违反了monad定律,因为第三个monad定律不成立:

m' = do x <- m
        f x
do y <- m'  /=  do x <- m
   g y             y <- f x
                   g y

但是,如果命令显式地发出"step"输出,则不存在冲突。这是因为return根本不能发出任何输出,而对两个命令进行排序只会将它们的阶跃输出相加。这里有一个例子:

import Control.Monad.Trans.State
import Control.Monad.Trans.Class (lift)
step :: StateT Int IO ()
step = do
    n <- get
    lift $ putStrLn $ "Step " ++ show n
    put (n + 1)
command1 = do
    command1'  -- The command1 logic without the step behavior
    step
command2 = do
    command2'
    step
-- etc.

请注意,我没有包括步骤的总数。monad无法访问这些信息。对于这个问题,我推荐Daniel的答案,因为Applicative是一个很好的解决方案,可以在没有任何TemplateHaskell的情况下静态地确定步骤数。

有很多记录器库。

如果你对Monad Logger感兴趣-这里是:Control.Monad.Logger

在Hackage中,您可以找到其他库

使用monad转换器在WriterT上堆叠,该WriterT计算有多少>>>>=已应用于底层monad。

最新更新