"вводить" отслеживание прогресса/отслеживание в вычислении 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
.