在Haskell中使用map时跳过异常

时间:2010-01-28 17:37:53

标签: haskell error-handling map

我有以下代码来返回字符串中循环的长度:

module Main where
import Data.List

detec ys n | 2*n > (length ys) = error "no cycle"
           | t == h = (2*n - n)
           | otherwise = detec ys (n+1)
            where
                t = ys !! n
                h = if n == 0 then ys !! 1 else  ys !! (n*2)
f x = detec (show x) 0
answer = map f [1/x|x<-[1..100]]

但是我不知道怎么做是让它忽略"no cycle"异常,这样生成的列表只包含循环字符串的长度。

我该怎么做?

3 个答案:

答案 0 :(得分:23)

请不要使用error来实现预计会出现“错误”结果的逻辑。

相反,为什么不返回Maybe n而不仅仅n,然后use catMaybes来过滤掉Nothing

这些变化很容易:

module Main where
import Data.List
import Data.Maybe

detec ys n | 2*n > (length ys) = Nothing
           | t == h = Just (2*n - n)
           | otherwise = detec ys (n+1)
            where
                t = ys !! n
                h = if n == 0 then ys !! 1 else  ys !! (n*2)

f x = detec (show x) 0
answer = catMaybes $ map f [1/x|x<-[1..100]]

顺便说一下,你是在列表的末尾索引;也许你打算检查2*n + 1 > length ys?稍微偏离主题,我想提一下,!!length在应用于列表时,大多数情况下都是低效和非惯用的,尤其是在像这样的迭代构造中。列表类型基本上是 cons单元列表,它是一种本质上递归的数据结构,并且强调数组。理想情况下,您应该避免使用无法使用模式匹配轻松表达的列表执行任何操作,例如f (x:xs) = ...

答案 1 :(得分:5)

您正在使用Project Euler #26吗?

仅仅因为某个数字重复,并不意味着你找到了一个循环:例如,在334/999 = 0.334334334 ......中,循环不是(3),它是(334)。另外,依靠浮点计算来给你足够准确的数字是不明智的:#26的解决方案绝对超出了浮点数给你的范围。

在任何情况下,都有更简单的方法来查找周期。这会保留以前看到的元素列表,当它在列表中向下走时,并在发现当前元素已经被看到时返回。

findCycle :: Eq a => [a] -> Int
findCycle = findCycle' [] where
    findCycle' _ [] = 0
    findCycle' k (x:xs) = maybe (findCycle' (x:k) xs) succ $ elemIndex x k

您的tortoise and hare算法不完整:可能无法始终找到最小的周期。这个缺陷在这里得到纠正。

findCycle :: Eq a => [a] -> Int
findCycle xs = findCycle' xs (tail xs) where
    findCycle' (x:xs) (y:_:ys)
      | x == y = fromJust (elemIndex x xs) + 1
      | otherwise = findCycle' xs ys

这假定它永远不会从列表末尾运行。如果您实现自己的十进制扩展,则可以轻松确保为真。

答案 2 :(得分:3)

您可以使用Control.Exception中的catch,如

import Prelude hiding (catch)
import Control.Exception

main = do
  print answer `catch` errorMessage
  where
    errorMessage :: SomeException -> IO ()
    errorMessage = putStrLn . ("error: " ++) . show

捕捉SomeException很草率,输出很乱:

[error: No cycle

它打印了一个数组,但遇到了异常。不是很好。

另一个答案涵盖了使用Maybe monad表示可能失败的计算的精细方法。更通用的方法是MonadError

{-# LANGUAGE FlexibleContexts #-}

import Control.Applicative
import Control.Monad.Error

detec2 :: (MonadError String m, Eq a) => [a] -> Int -> m Int
detec2 ys n | 2*n >= (length ys) = throwError "No cycle"
            | t == h = return (2*n - n)
            | otherwise = detec2 ys (n+1)
             where
                 t = ys !! n
                 h = if n == 0 then ys !! 1 else  ys !! (n*2)

(请注意,这也解决了第一个警卫中允许!!抛出异常的错误。)

这允许类似但更灵活的使用,例如:

answer2 = f2 <$> [1/x | x <- [1..100]]

f2 x = detec2 (show x) 0

main = do
  forM_ answer2 $
    \x -> case x of
            Left msg -> putStrLn $ "error: " ++ msg
            Right x  -> print x

现在输出的前几行是

error: No cycle
error: No cycle
2
error: No cycle
error: No cycle
3
6
error: No cycle
2

请记住,这仍然是一个纯粹的功能:您不必在IO内运行它。要忽略无周期错误,可以使用

cycles :: [Int]
cycles = [x | Right x <- answer2]

如果您根本不关心错误消息,则不要生成它们。一种自然的方法是使用列表返回空列表中没有循环,并使用concatMap压缩结果:

detec3 :: (Show a) => a -> [Int]
detec3 x = go 0
  where go :: Int -> [Int]
        go n
          | 2*n >= len = []
          |     t == h = [2*n - n]
          |  otherwise = go (n+1)
          where t = ys !! n
                h | n == 0    = ys !! 1
                  | otherwise = ys !! (n*2)
                len = length ys
                ys = show x

main = do
  print $ concatMap (detec3 . recip) [1..100]

最后,您可能有兴趣阅读8 ways to report errors in Haskell