尴尬的 Monad 变压器堆栈

发布于 2024-08-04 15:11:50 字数 2566 浏览 5 评论 0原文

解决 Google Code Jam 中的问题 (2009.1AA:“Multi-基本幸福”)我想出了一个尴尬的(代码方面的)解决方案,我对如何改进它很感兴趣。

简而言之,问题描述是:对于给定列表中的所有碱基,找到大于 1 的最小数字,迭代计算数字平方和达到 1。

或伪 Haskell 中的描述(如果 elem 始终适用于无限列表,则可以解决该问题的代码):

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

我的尴尬解决方案:

  • 尴尬的意思是它有这种代码: happy < ;- 举起 。举起 。 lift $ isHappy Set.empty base cur
  • 我记住 isHappy 函数的结果。使用 State monad 来存储结果 Map。
  • 为了找到第一个解决方案,我没有使用 headfilter (就像上面的伪 haskell 那样),因为计算不是纯粹的(改变状态)。因此,我使用 StateT 和计数器进行迭代,并使用 MaybeT 在条件成立时终止计算。
  • 已经在 MaybeT (StateT a (State b)) 中,如果某个碱基的条件不成立,则无需检查其他碱基,因此我有另一个 MaybeT< /code> 在堆栈中。

代码:

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 的其他参赛者确实有更好的解决方案 ,但解决问题的方式不同。我的问题是关于对我的代码进行小的迭代改进。

Solving a problem from Google Code Jam (2009.1A.A: "Multi-base happiness") I came up with an awkward (code-wise) solution, and I'm interested in how it could be improved.

The problem description, shortly, is: Find the smallest number bigger than 1 for which iteratively calculating the sum of squares of digits reaches 1, for all bases from a given list.

Or description in pseudo-Haskell (code that would solve it if elem could always work for infinite lists):

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

And my awkward solution:

  • By awkward I mean it has this kind of code: happy <- lift . lift . lift $ isHappy Set.empty base cur
  • I memoize results of the isHappy function. Using the State monad for the memoized results Map.
  • Trying to find the first solution, I did not use head and filter (like the pseudo-haskell above does), because the computation isn't pure (changes state). So I iterated by using StateT with a counter, and a MaybeT to terminate the computation when condition holds.
  • Already inside a MaybeT (StateT a (State b)), if the condition doesn't hold for one base, there is no need to check the other ones, so I have another MaybeT in the stack for that.

Code:

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

Other contestants using Haskell did have nicer solutions, but solved the problem differently. My question is about small iterative improvements to my code.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

不如归去 2024-08-11 15:11:50

您的解决方案在使用(和滥用) monad 方面肯定很尴尬:

  • 通常通过堆叠多个变压器来零碎地构建 monad
  • 堆叠多个状态的情况不太常见,但有时仍然会发生
  • 堆叠几个 Maybe 变压器是非常不寻常
  • 的更不寻常的是使用 MaybeT 中断循环

您的代码有点太无意义了:

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

而不是更容易阅读

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

现在关注函数solve1,让我们简化它。
一个简单的方法是删除内部 MaybeT monad。您可以反其道而行之,仅当满足以下条件时才递归,而不是在找到满意的数字时中断的永远循环
数不高兴。

此外,您也并不真正需要 State monad,不是吗?人们总是可以用明确的参数来代替状态。

应用这些想法solve1现在看起来好多了:

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

Your solution is certainly awkward in its use (and abuse) of monads:

  • It is usual to build monads piecemeal by stacking several transformers
  • It is less usual, but still happens sometimes, to stack several states
  • It is very unusual to stack several Maybe transformers
  • It is even more unusual to use MaybeT to interrupt a loop

Your code is a bit too pointless :

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

instead of the easier to read

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

Focusing now on function solve1, let us simplify it.
An easy way to do so is to remove the inner MaybeT monad. Instead of a forever loop which breaks when a happy number is found, you can go the other way around and recurse only if the
number is not happy.

Moreover, you don't really need the State monad either, do you ? One can always replace the state with an explicit argument.

Applying these ideas solve1 now looks much better:

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)

I would be more han happy with that code.
The rest of your solution is fine.
One thing that bothers me is that you throw away the memo cache for every subproblem. Is there a reason for that?

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"

Wouldn't your solution be more efficient if you reused it instead ?

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
清风挽心 2024-08-11 15:11:50

Monad* 类的存在是为了消除重复提升的需要。如果您像这样更改签名:

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

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

这样您就可以删除大部分“电梯”。但是,最长的提升序列无法删除,因为它是 StateT 内的 State monad,因此使用 MonadState 类型类将为您提供外部 StateT,您需要在其中访问内部 State。您可以将 State monad 包装在新类型中并创建一个 MonadHappy 类,类似于现有的 monad 类。

The Monad* classes exist to remove the need for repeated lifting. If you change your signatures like this:

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

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

This way you can remove most of the 'lift's. However, the longest sequence of lifts cannot be removed, since it is a State monad inside a StateT, so using the MonadState type class will give you the outer StateT, where you need tot get to the inner State. You could wrap your State monad in a newtype and make a MonadHappy class, similar to the existing monad classes.

夏末的微笑 2024-08-11 15:11:50

ListT (来自 List 包)比 < code>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 monad 中,但如果我们想获得结果状态,我们' d 有一个问题,因为 filterM 运行它为 [2..](一个无限列表)的每个元素获取的单子谓词。

对于一元列表,filterL cond (fromList [2..]) 表示一个列表,我们可以将其作为一元操作一次访问一项,因此我们的一元谓词 cond 除非我们消耗相应的列表项,否则实际上不会执行(并影响状态)。

类似地,如果我们已经从 isHappy 之一获得了 False 结果,则使用 andL 实现 cond 会使我们不再计算和更新状态。 Set.empty num计算。

ListT (from the List package) does a much nicer job than MaybeT in stopping the calculation when necessary.

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

Some elaboration on how this works:

Had we used a regular list the code would had looked like this:

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

This calculation happens in a State monad, but if we'd like to get the resulting state, we'd have a problem, because filterM runs the monadic predicate it gets for every element of [2..], an infinite list.

With the monadic list, filterL cond (fromList [2..]) represents a list that we can access one item at a time as a monadic action, so our monadic predicate cond isn't actually executed (and affecting the state) unless we consume the corresponding list items.

Similarly, implementing cond using andL makes us not calculate and update the state if we already got a False result from one of the isHappy Set.empty num calculations.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文