在haskell计算中"注入"进度记录/跟踪?

Jus*_* L. 15 haskell referential-transparency

我正在挑选一个特定的任务来说明我在说什么

假设我想通过检查下面的每个数字(如果它是一个因子,然后将它们加在一起)来天真地找到大数的所有因子的总和.

在命令式编程语言中,IO和纯计算之间没有分离,你可能会做这样的事情

def sum_of_factors(n):
  sum = 0
  for i between 1 and n:
    if (n % i == 0):
      sum += i
  return sum
Run Code Online (Sandbox Code Playgroud)

但是如果我n很大,我最终会在计算完成之前长时间盯着空屏幕.所以我添加一些日志记录 -

def sum_of_factors(n):
  sum = 0
  for i between 1 and n:
    if (i % 1000 == 0):
      print "checking $i..."
    if (n % i == 0):
      print "found factor $i"
      sum += 1
  return sum
Run Code Online (Sandbox Code Playgroud)

实际上,这个增加是微不足道的.

现在,如果我要在教科书中使用haskell,我可能会这样做

sum_of_factors :: Int -> Int
sum_of_factors n = foldl' (+) 0 factors
  where
    factors = filter ((== 0) . (mod n)) [1..n]
Run Code Online (Sandbox Code Playgroud)

我遇到了和以前一样的问题......对于大数字,我只是盯着空白的屏幕看了一会儿.

但我无法弄清楚如何在Haskell代码中注入相同类型的跟踪/日志记录.除了可能使用显式递归重新实现折叠之外,我不确定获得与命令式不纯代码相同的跟踪模式/结果.

Haskell有一个教员可以做到这一点吗?一个不需要重构一切的东西?

谢谢

Yur*_*ras 14

有许多可能的解决方案.

最简单的方法是更改​​函数以返回事件流而不是最终结果.你sum_of_factors不为我编译,所以我将使用一个sum函数作为例子.想法是发送Left message以显示进度,并Right result在完成后发送.感谢懒惰评估,您将在功能正常工作时看到进度事件:

import Control.Monad

sum' :: [Int] -> [Either String Int]
sum' = go step 0
  where
  step = 10000
  go _ res [] = [Right res]
  go 0 res (x:xs) = Left ("progress: " ++ show x) : go step (res + x) xs
  go c res (x:xs) = go (c - 1) (res + x) xs

main :: IO ()
main = do
  forM_ (sum' [1..1000000]) $ \event ->
    case event of
      Right res -> putStrLn $ "Result: " ++ show res
      Left str -> putStrLn str
Run Code Online (Sandbox Code Playgroud)

其他(从我的观点来看更好)解决方案是使函数monadic:

class Monad m => LogM m where
  logMe :: String -> m ()

instance LogM IO where
  logMe = putStrLn

sum' :: LogM m => [Int] -> m Int
sum' = go step 0
  where
  step = 10000
  go _ res [] = return res
  go 0 res (x:xs) = logMe ("progress: " ++ show x) >> go step (res + x) xs
  go c res (x:xs) = go (c - 1) (res + x) xs

main :: IO ()
main = sum' [1..1000000] >>= print
Run Code Online (Sandbox Code Playgroud)

或使用foldM:

import Control.Monad

sum' :: LogM m => [Int] -> m Int
sum' = liftM snd . foldM go (0, 0)
  where
    step = 10000
    -- `!` forces evaluation and prevents build-up of thunks.
    -- See the BangPatterns language extension.
    go (!c, !res) x = do
        when (c == 0) $ logMe ("progress: " ++ show x)
        return $ ((c + 1) `mod` step, res + x)
Run Code Online (Sandbox Code Playgroud)

  • @JustinL.您可以使用[foldM](http://hackage.haskell.org/packages/archive/base/latest/doc/html/Control-Monad.html#v:foldM)来避免显式递归. (2认同)
  • @JustinL.记录是副作用,因此您必须通过流式传输模拟它或通过monadic上下文显式定义.您可以使用`enableLoggin`,`disableLoggin`,`withoutLogging`等方法扩展`LogM`来创建可配置的日志框架.甚至可以在编译时启用/禁用日志记录,如@PetrPudlák所建议的那样.注意:"monadic"并不意味着"不纯". (2认同)

Pet*_*lák 10

如果需要快速和脏的日志记录,可以使用Debug.Trace.它允许您快速将日志记录功能添加到纯代码中.(当然,它使用不安全的东西.)准备好它的日志记录输出出现在你预期的不同时间(或根本不出现) - 这是将不纯的调试代码添加到懒惰的纯计算中的结果评估.

否则,您必须使用monadic代码,以便正确排序日志输出.其中一个使用良好的库IOhslogger.

如果您不想将代码绑定到IO(这是非常明智的),Yuras的方法是可行的方法.创建自己的monad类型类,描述您的日志记录操作(可能具有不同的级别等).然后,有一个生成日志输出的实例,比如答案,以及一个不做任何事情的实例,比如

instance LogM Identity where
  logMe _ = return ()
Run Code Online (Sandbox Code Playgroud)

然后,只需切换你正在使用的monad,就可以打开/关闭loggin,编译器会优化Identitymonad.