неуклюжий стек трансформатора монад

Решение проблемы из Google Code Jam (2009.1AA: "Multi- базовое счастье ") Я придумал неудобное (с точки зрения кода) решение, и мне интересно, как его можно улучшить.

Краткое описание проблемы: найти наименьшее число больше 1, для которого итеративное вычисление суммы квадратов цифр достигает 1 для всех оснований из данного списка.

Или описание в псевдо-Haskell (код, который решил бы это, если бы elem всегда мог работать для бесконечных списков):

solution =
  head . (`filter` [2..]) .
  all ((1 `elem`) . (`iterate` i) . sumSquareOfDigitsInBase)

И мое неудобное решение:

  • Под неудобным я имею в виду такой код: happy <- lift . lift . lift $ isHappy Set.empty base cur
  • Запоминаю результаты функции isHappy. Использование монады State для запоминания результатов Map.
  • Пытаясь найти первое решение, я не использовал head и filter (как это делает псевдо-haskell выше), потому что вычисление не является чистым (меняет состояние). Поэтому я повторил попытку, используя StateT со счетчиком и MaybeT, чтобы завершить вычисление, когда условие выполняется.
  • Уже внутри MaybeT (StateT a (State b)), если условие не выполняется для одной базы, нет необходимости проверять другие, поэтому у меня есть еще MaybeT в стеке для этого.

Код:

import Control.Monad.Maybe
import Control.Monad.State
import Data.Maybe
import qualified Data.Map as Map
import qualified Data.Set as Set

type IsHappyMemo = State (Map.Map (Integer, Integer) Bool)

isHappy :: Set.Set Integer -> Integer -> Integer -> IsHappyMemo Bool
isHappy _ _ 1 = return True
isHappy path base num = do
  memo <- get
  case Map.lookup (base, num) memo of
    Just r -> return r
    Nothing -> do
      r <- calc
      when (num < 1000) . modify $ Map.insert (base, num) r
      return r
  where
    calc
      | num `Set.member` path = return False
      | otherwise = isHappy (Set.insert num path) base nxt
    nxt =
      sum . map ((^ (2::Int)) . (`mod` base)) .
      takeWhile (not . (== 0)) . iterate (`div` base) $ num

solve1 :: [Integer] -> IsHappyMemo Integer
solve1 bases =
  fmap snd .
  (`runStateT` 2) .
  runMaybeT .
  forever $ do
    (`when` mzero) . isJust =<<
      runMaybeT (mapM_ f bases)
    lift $ modify (+ 1)
  where
    f base = do
      cur <- lift . lift $ get
      happy <- lift . lift . lift $ isHappy Set.empty base cur
      unless happy mzero

solve :: [String] -> String
solve =
  concat .
  (`evalState` Map.empty) .
  mapM f .
  zip [1 :: Integer ..]
  where
    f (idx, prob) = do
      s <- solve1 . map read . words $ prob
      return $ "Case #" ++ show idx ++ ": " ++ show s ++ "\n"

main :: IO ()
main =
  getContents >>=
  putStr . solve . tail . lines

У других участников, использующих Haskell, действительно были более хорошие решения, но решил проблему иначе. Мой вопрос касается небольших итеративных улучшений моего кода.


person yairchu    schedule 17.09.2009    source источник


Ответы (3)


Ваше решение определенно неудобно в использовании (и злоупотреблении) монадами:

  • Обычно монады строят по частям, складывая несколько трансформаторов.
  • Менее обычно, но все же иногда случается складывать несколько состояний
  • Сложить несколько трансформаторов Maybe очень необычно.
  • Еще более необычно использовать MaybeT для прерывания цикла

Ваш код слишком бессмысленен:

(`when` mzero) . isJust =<<
   runMaybeT (mapM_ f bases)

вместо того, чтобы легче читать

let isHappy = isJust $ runMaybeT (mapM_ f bases)
when isHappy mzero

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

Более того, вам ведь тоже не нужна монада State? Всегда можно заменить состояние явным аргументом.

Применение этих идей solution1 теперь выглядит намного лучше:

solve1 :: [Integer] -> IsHappyMemo Integer
solve1 bases = go 2 where
  go i = do happyBases <- mapM (\b -> isHappy Set.empty b i) bases
            if and happyBases
              then return i
              else go (i+1)

Я был бы более доволен этим кодом. Остальное ваше решение в порядке. Меня беспокоит то, что вы выбрасываете кеш-память для каждой подзадачи. Есть ли для этого причина?

solve :: [String] -> String
 solve =
    concat .
    (`evalState` Map.empty) .
    mapM f .
   zip [1 :: Integer ..]
  where
    f (idx, prob) = do
      s <- solve1 . map read . words $ prob
      return $ "Case #" ++ show idx ++ ": " ++ show s ++ "\n"

Разве ваше решение не было бы более эффективным, если бы вы вместо этого использовали его повторно?

solve :: [String] -> String
solve cases = (`evalState` Map.empty) $ do
   solutions <- mapM f (zip [1 :: Integer ..] cases)
   return (unlines solutions)
  where
    f (idx, prob) = do
      s <- solve1 . map read . words $ prob
      return $ "Case #" ++ show idx ++ ": " ++ show s
person pepeiborra    schedule 18.09.2009
comment
@pepeiborra: отличный ответ, но я думаю, что есть некоторые проблемы. Имхо, я не выбрасываю кеш-память, как вы говорите. вы можете возвращать решения вместо решений с удалением строк и применять отмену строк к результату evalState. тогда вместо решения ‹- ...; возвращать решения, которые вы можете просто сделать .... это то, что я делаю ... также вы вычисляете isHappy во всех базах, даже если один из них вернул False .. - person yairchu; 19.09.2009
comment
Правильно, я не уделил достаточно внимания, ваш код не выбрасывает кеш-память. Рег. для всех баз, монада State по умолчанию является ленивой, поэтому следует рассчитывать только те базы, которые действительно необходимы. - person pepeiborra; 19.09.2009
comment
@pepeiborra: рег. все основания, я не думаю, что лень Государственной монады решает это. это по-прежнему влияет на состояние, и в следующий раз, когда это состояние понадобится (следующий вызов решения1), эти вычисления потребуются для его вычисления. - person yairchu; 20.09.2009
comment
@yairchu, здесь происходят две вещи. Во-первых, поскольку монада состояния по умолчанию является ленивой, эффект вычисления (обновление состояния) приостанавливается до тех пор, пока он действительно не понадобится. В этом случае состояние действительно необходимо, когда в следующий раз будет вызван detect1, так что да, эти вызовы isHappy имеют место в остальных базах. Но во-вторых, поскольку само состояние является ленивым, поскольку Data.Map ленив в значениях (и, как мне кажется, строг в ключах), эти дополнительные вызовы isHappy приостанавливаются и никогда не получают возможности запускаться, если это действительно не нужно. - person pepeiborra; 20.09.2009
comment
@yairchu еще одна попытка, на этот раз с кодом: hpaste.org/fastcgi/ hpaste.fcgi / view? id = 9612 - person pepeiborra; 20.09.2009

Классы Monad * существуют для устранения необходимости повторного подъема. Если вы измените свои подписи следующим образом:

type IsHappyMemo = Map.Map (Integer, Integer) Bool

isHappy :: MonadState IsHappyMemo m => Set.Set Integer -> Integer -> Integer -> m Bool

Таким образом можно убрать большую часть «лифта». Однако самая длинная последовательность лифтов не может быть удалена, поскольку это монада State внутри StateT, поэтому использование класса типа MonadState даст вам внешний StateT, из которого вам нужно перейти во внутреннее состояние. Вы можете обернуть свою монаду State в новый тип и создать класс MonadHappy, аналогичный существующим классам монад.

person Erik Hesselink    schedule 18.09.2009

ListT (из пакета List) выполняет гораздо более приятную работу, чем MaybeT, останавливая вычисление при необходимости.

solve1 :: [Integer] -> IsHappyMemo Integer
solve1 bases = do
  Cons result _ <- runList . filterL cond $ fromList [2..]
  return result
  where
    cond num = andL . mapL (isHappy Set.empty num) $ fromList bases

Некоторые пояснения того, как это работает:

Если бы мы использовали обычный список, код выглядел бы так:

solve1 bases = do
  result:_ <- filterM cond [2..]
  return result
  where
    cond num = fmap and . mapM (isHappy Set.empty num) bases

Это вычисление происходит в монаде State, но если мы захотим получить результирующее состояние, у нас возникнет проблема, потому что filterM запускает монадический предикат, который он получает для каждого элемента [2..], бесконечного списка.

В монадическом списке filterL cond (fromList [2..]) представляет список, к которому мы можем обращаться к одному элементу за раз как монадическое действие, поэтому наш монадический предикат cond фактически не выполняется (и не влияет на состояние), если мы не потребляем соответствующие элементы списка.

Точно так же реализация cond с использованием andL заставляет нас не вычислять и обновлять состояние, если мы уже получили False результат одного из isHappy Set.empty num вычислений.

person yairchu    schedule 22.02.2011
comment
Не могли бы вы подробнее объяснить, как можно остановить расчет? - person Johannes Gerer; 01.06.2013