我甚至不确定任何一种单子都有可能;它违反了monad法律吗?但似乎在某种构造或其他方面应该是可能的。具体来说,有什么方法可以写出像
这样的东西do
someOp ()
someOtherOp ()
thirdOp ()
它会打印
step 1 of 3
step 2 of 3
step 3 of 3
这需要模板Haskell还是monad工作? (如果需要模板Haskell,那么该怎么做?)
答案 0 :(得分:10)
我假设您希望自动显示这些步骤,而不必使用日志语句为您编写代码。
使用monad执行此操作的问题在于它们太灵活:在任何时候,其余计算的“形状”可能取决于计算本身期间获得的值。这在(>>=)
的类型中明确显示,即m a -> (a -> m b) -> m b
。
因此,在运行计算之前,您可以知道总步数的固定数量N
。
然而,Haskell提供了另外两个抽象,它们交换了monad的一些功能和灵活性,以便有机会事先执行更多的“静态”分析:applicative functors和arrows。
适用的仿函数虽然非常有用,但对你的需求来说可能太“弱”了。您无法在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)
答案 4 :(得分:-1)
使用monad transformer堆叠在WriterT上,该WriterT计算已将>>
和>>=
应用于基础monad的数量。