"вводить" отслеживание прогресса/отслеживание в вычислении haskell?

Я собираю конкретную задачу, чтобы проиллюстрировать, о чем я говорил

Скажем, я хотел найти сумму всех факторов большого числа наивно - путем проверки каждого числа под ним, если это был фактор, затем сложение их вместе.

На императивном языке программирования без разделения между IO и чистыми вычислениями вы можете сделать что-то вроде этого

def sum_of_factors(n):
  sum = 0
  for i between 1 and n:
    if (n % i == 0):
      sum += i
  return sum

Однако, если мой 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

и действительно, это добавление тривиально.

Теперь, если бы я сделал это в учебнике haskell, я мог бы сделать

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

Я столкнулся с той же проблемой, что и раньше... для больших чисел я просто смотрю на пустой экран некоторое время.

Но я не могу понять, как вводить такой же трассировки/протоколирования в код Haskell. я не уверен, кроме, возможно, повторного внедрения fold с явной рекурсией, чтобы получить тот же шаблон трассировки/результат, что и в императивном нечистом коде.

Есть ли в Haskell факультет, чтобы сделать это выполнимым? Тот, который не требует рефакторинга всего?

Спасибо

Ответы

Ответ 1

Существует ряд возможных решений.

Самый простой - это изменить вашу функцию, чтобы возвращать поток событий вместо конечного результата. Вы 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

Другое (и лучше с моей точки зрения) решение состоит в том, чтобы сделать функцию монодичной:

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

или используя 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)

Ответ 2

Если вам нужны быстрые и грязные записи, вы можете использовать Debug.Trace. Это позволяет вам быстро добавлять функции регистрации в чистый код. (Разумеется, под капотом для этого используется небезопасный материал.) Будьте готовы к тому, что вывод журнала будет отображаться в разное время, чем вы ожидаете (или вообще нет) - это следствие добавления нечистого кода отладки в чистые вычисления, которые ленивы оценены.

В противном случае вы должны использовать монадический код, чтобы правильно выполнить вывод журнала. Одна из хорошо развитых библиотек, использующая IO, - hslogger.

Если вы не хотите привязывать свой код к IO (что очень разумно), подход Yuras - это путь. Создайте свой собственный тип типа монады, который описывает ваши операции регистрации (возможно, с разными уровнями и т.д.). Затем, один экземпляр, который производит вывод журнала, как в ответе, и один экземпляр, который ничего не делает, например

instance LogM Identity where
  logMe _ = return ()

Затем, просто переключив монаду, с которой вы работаете, вы включаете/выключаете loggin, а компилятор оптимизирует монаду Identity.