如何在monad中执行每个语句时写一个打印“N的步骤i”的monad?

时间:2013-11-29 20:05:11

标签: haskell monads template-haskell

我甚至不确定任何一种单子都有可能;它违反了monad法律吗?但似乎在某种构造或其他方面应该是可能的。具体来说,有什么方法可以写出像

这样的东西
do
  someOp ()
  someOtherOp ()
  thirdOp ()

它会打印

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

这需要模板Haskell还是monad工作? (如果需要模板Haskell,那么该怎么做?)

5 个答案:

答案 0 :(得分:10)

我假设您希望自动显示这些步骤,而不必使用日志语句为您编写代码。

使用monad执行此操作的问题在于它们灵活:在任何时候,其余计算的“形状”可能取决于计算本身期间获得的值。这在(>>=)的类型中明确显示,即m a -> (a -> m b) -> m b

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

然而,Haskell提供了另外两个抽象,它们交换了monad的一些功能和灵活性,以便有机会事先执行更多的“静态”分析:applicative functorsarrows

适用的仿函数虽然非常有用,但对你的需求来说可能太“弱”了。您无法在applicative functor中编写一个函数,当应用于某个值时,该函数会将该值打印到控制台。文章"Idioms are oblivious, arrows are meticulous, monads are promiscuous"中对此进行了解释,其中包含了每个抽象限制的一些有启发性的例子(在该论文中,应用函子称为“习语”。)

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

所以,如果你能用Kleisli arrows(monad的箭头)来表达你的计算,也许你可以写一些箭头变换器( monad变换器),它增加了自动记录能力。

arrows包提供了许多箭头变换器。我认为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步中产生的值的影响。使用应用程序无法完成。

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

显示执行步骤需要更多工作。

编辑#2 如果我们正在处理一系列命令,其中没有效果取决于前一个效果产生的值,那么我们可以避免使用箭头并仅使用applicative functors计算步数:

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)

答案 1 :(得分:5)

虽然我认为DanielDí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 ()
   ]

答案 2 :(得分:3)

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

例如,如果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

但是,如果您有命令显式发出“步骤”输出,则没有违规。这是因为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是一个很好的解决方案,可以在没有任何模板Haskell的情况下静态确定步骤数。

答案 3 :(得分:1)

有很多记录器库。

如果您对Monad-Logger感兴趣,请访问:Control.Monad.Logger

Hackage你可以找到其他图书馆

答案 4 :(得分:-1)

使用monad transformer堆叠在WriterT上,该WriterT计算已将>>>>=应用于基础monad的数量。