问题类似于this问题。但是,这个是关于异常的,而不是关于懒惰的I / O.
这是一个测试:
{-# LANGUAGE ScopedTypeVariables #-}
import Prelude hiding ( catch )
import Control.Exception
fooLazy :: Int -> IO Int
fooLazy m = return $ 1 `div` m
fooStrict :: Int -> IO Int
fooStrict m = return $! 1 `div` m
test :: (Int -> IO Int) -> IO ()
test f = print =<< f 0 `catch` \(_ :: SomeException) -> return 42
testLazy :: Int -> IO Int
testLazy m = (return $ 1 `div` m) `catch` \(_ :: SomeException) -> return 42
testStrict :: Int -> IO Int
testStrict m = (return $! 1 `div` m) `catch` \(_ :: SomeException) -> return 42
所以我写了两个函数fooLazy
,它们是懒惰的,fooStrict
是严格的,还有两个测试testLazy
和testStrict
,然后我试着抓住除以零:
> test fooLazy
*** Exception: divide by zero
> test fooStrict
42
> testLazy 0
*** Exception: divide by zero
> testStrict 0
42
并且它在懒惰的情况下失败。
首先想到的是编写catch
函数的一个版本,强制对第一个参数进行评估:
{-# LANGUAGE ScopedTypeVariables #-}
import Prelude hiding ( catch )
import Control.DeepSeq
import Control.Exception
import System.IO.Unsafe
fooLazy :: Int -> IO Int
fooLazy m = return $ 1 `div` m
fooStrict :: Int -> IO Int
fooStrict m = return $! 1 `div` m
instance NFData a => NFData (IO a) where
rnf = rnf . unsafePerformIO
catchStrict :: (Exception e, NFData a) => IO a -> (e -> IO a) -> IO a
catchStrict = catch . force
test :: (Int -> IO Int) -> IO ()
test f = print =<< f 0 `catchStrict` \(_ :: SomeException) -> return 42
testLazy :: Int -> IO Int
testLazy m = (return $ 1 `div` m) `catchStrict` \(_ :: SomeException) -> return 42
testStrict :: Int -> IO Int
testStrict m = (return $! 1 `div` m) `catchStrict` \(_ :: SomeException) -> return 42
似乎有效:
> test fooLazy
42
> test fooStrict
42
> testLazy 0
42
> testStrict 0
42
但我在这里使用unsafePerformIO
功能,这很可怕。
我有两个问题:
catch
函数始终捕获所有异常,无论其第一个参数的性质如何?catchStrict
函数的东西是合适的吗?更新1 。
这是nanothief的catchStrict
函数的更好版本:
forceM :: (Monad m, NFData a) => m a -> m a
forceM m = m >>= (return $!) . force
catchStrict :: (Exception e, NFData a) => IO a -> (e -> IO a) -> IO a
catchStrict expr = (forceM expr `catch`)
更新2 。
这是另一个“坏”的例子:
main :: IO ()
main = do
args <- getArgs
res <- return ((+ 1) $ read $ head args) `catch` \(_ :: SomeException) -> return 0
print res
应该像这样重写:
main :: IO ()
main = do
args <- getArgs
print ((+ 1) $ read $ head args) `catch` \(_ :: SomeException) -> print 0
-- or
--
-- res <- return ((+ 1) $ read $ head args) `catchStrict` \(_ :: SomeException) -> return 0
-- print res
--
-- or
--
-- res <- returnStrcit ((+ 1) $ read $ head args) `catch` \(_ :: SomeException) -> return 0
-- print res
--
-- where
returnStrict :: Monad m => a -> m a
returnStrict = (return $!)
更新3 。
当注意到nanothief时,无法保证catch
函数始终捕获任何异常。所以需要仔细使用它。
关于如何解决相关问题的一些提示:
($!)
与return
一起使用,在forceM
的第一个参数上使用catch
,使用catchStrict
函数。以下是一个例子:
{-# LANGUAGE GeneralizedNewtypeDeriving, TypeSynonymInstances, FlexibleInstances
, MultiParamTypeClasses, UndecidableInstances, ScopedTypeVariables #-}
import System.Environment
import Prelude hiding ( IO )
import qualified Prelude as P ( IO )
import qualified Control.Exception as E
import Data.Foldable
import Data.Traversable
import Control.Applicative
import Control.Monad.Trans
import Control.Monad.Error
newtype StrictT m a = StrictT { runStrictT :: m a } deriving
( Foldable, Traversable, Functor, Applicative, Alternative, MonadPlus, MonadFix
, MonadIO
)
instance Monad m => Monad (StrictT m) where
return = StrictT . (return $!)
m >>= k = StrictT $ runStrictT m >>= runStrictT . k
fail = StrictT . fail
instance MonadTrans StrictT where
lift = StrictT
type IO = StrictT P.IO
instance E.Exception e => MonadError e IO where
throwError = StrictT . E.throwIO
catchError m h = StrictT $ runStrictT m `E.catch` (runStrictT . h)
io :: StrictT P.IO a -> P.IO a
io = runStrictT
它基本上是the identity monad transformer,但是严格return
:
foo :: Int -> IO Int
foo m = return $ 1 `div` m
fooReadLn :: Int -> IO Int
fooReadLn x = liftM (`div` x) $ liftIO readLn
test :: (Int -> IO Int) -> P.IO ()
test f = io $ liftIO . print =<< f 0 `catchError` \(_ :: E.SomeException) -> return 42
main :: P.IO ()
main = io $ do
args <- liftIO getArgs
res <- return ((+ 1) $ read $ head args) `catchError` \(_ :: E.SomeException) -> return 0
liftIO $ print res
-- > test foo
-- 42
-- > test fooReadLn
-- 1
-- 42
-- ./main
-- 0
答案 0 :(得分:9)
首先(我不确定你是否已经知道这一点),抓住这个懒惰案例的原因是
1 `div` 0
表达式在需要之前不会被评估,这在print
函数内部。但是,catch
方法仅适用于f 0
表达式,而不是整个print =<< f 0
表达式,因此不会捕获异常。如果你这样做了:
test f = (print =<< f 0) `catch` \(_ :: SomeException) -> print 42
相反,它在两种情况下都能正常工作。
如果你想制作一个catch语句,虽然强制完全评估IO结果,而不是创建一个新的NFData实例,你可以写一个forceM
方法,并在catchStrict
中使用它方法:
forceM :: (Monad m, NFData a) => m a -> m a
forceM m = m >>= (return $!) . force
catchStrict :: (Exception e, NFData a) => IO a -> (e -> IO a) -> IO a
catchStrict expr = (forceM expr `catch`)
(我有点惊讶forceM不在Control.DeepSeq
库中)
关于你的评论:
不,规则是仅在计算值时抛出异常,并且仅在haskell需要时执行。如果haskell可以推迟对它的评估。
一个示例测试函数,它不使用$!
,但仍会立即引发异常(因此正常的catch会捕获除以零的异常):
fooEvaluated :: Int -> IO Int
fooEvaluated m = case 3 `div` m of
3 -> return 3
0 -> return 0
_ -> return 1
Haskell被迫评估“3`div`m”表达式,因为它需要将结果与3和0匹配。
作为最后一个例子,以下内容不会抛出任何异常,并且当与test函数一起使用时返回1:
fooNoException :: Int -> IO Int
fooNoException m = case 3 `div` m of
_ -> return 1
这是因为haskell永远不需要计算“3`div`m”表达式(因为_
匹配所有内容),所以它永远不会被计算,因此不会抛出任何异常。