懒惰和异常如何在Haskell中协同工作?

时间:2012-06-22 03:42:33

标签: exception haskell ghc lazy-evaluation

问题类似于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是严格的,还有两个测试testLazytestStrict,然后我试着抓住除以零:

> 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功能,这很可怕。

我有两个问题:

  1. 可以确定catch函数始终捕获所有异常,无论其第一个参数的性质如何?
  2. 如果没有,是否有一种众所周知的方法来处理这类问题?类似catchStrict函数的东西是合适的吗?

  3. 更新1

    这是nanothiefcatchStrict函数的更好版本:

    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函数始终捕获任何异常。所以需要仔细使用它。

    关于如何解决相关问题的一些提示:

    1. ($!)return一起使用,在forceM的第一个参数上使用catch,使用catchStrict函数。
    2. 我也注意到有时人们会add some strictness对他们的变形金刚的实例。
    3. 以下是一个例子:

      {-# 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
      

1 个答案:

答案 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”表达式(因为_匹配所有内容),所以它永远不会被计算,因此不会抛出任何异常。