2012-06-22 39 views
9

Câu hỏi tương tự như câu hỏi this. Tuy nhiên, đây là một ngoại lệ, không phải về I/O lười biếng.Làm thế nào để lười biếng và ngoại lệ làm việc cùng nhau trong Haskell?

Dưới đây là một thử nghiệm:

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

Vì vậy, tôi đã viết hai chức năng fooLazy đó là lười biếng và fooStrict đó là nghiêm ngặt, cũng có hai bài kiểm tra testLazytestStrict, sau đó tôi cố gắng nắm bắt phép chia cho không:

> test fooLazy 
*** Exception: divide by zero 
> test fooStrict 
42 
> testLazy 0 
*** Exception: divide by zero 
> testStrict 0 
42 

và không thành công trong trường hợp lười.

Việc đầu tiên mà nói đến cái tâm là để viết một phiên bản của catch chức năng buộc các đánh giá trên số đầu tiên của nó:

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

nó dường như làm việc:

> test fooLazy 
42 
> test fooStrict 
42 
> testLazy 0 
42 
> testStrict 0 
42 

nhưng tôi sử dụng hàm unsafePerformIO ở đây và điều này thật đáng sợ.

Tôi có hai câu hỏi:

  1. người ta có thể chắc chắn rằng catch chức năng luôn bắt tất cả các trường hợp ngoại lệ, không phụ thuộc vào bản chất của nó đối số đầu tiên?
  2. Nếu không, có cách nào nổi tiếng để giải quyết vấn đề này không? Một cái gì đó giống như chức năng catchStrict là phù hợp?

UPDATE 1.

Đây là một phiên bản tốt hơn của catchStrict chức năng bởi nanothief:

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`) 

UPDATE 2.

Dưới đây là một 'xấu' dụ:

main :: IO() 
main = do 
    args <- getArgs 
    res <- return ((+ 1) $ read $ head args) `catch` \(_ :: SomeException) -> return 0 
    print res 

Nó nên được viết lại như thế này:

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 $!) 

CẬP NHẬT 3.

Vì đã nhận thấy nanothief, không đảm bảo rằng chức năng catch luôn bắt bất kỳ ngoại lệ nào. Vì vậy, một trong những cần phải sử dụng nó một cách cẩn thận.

Vài lời khuyên về cách giải quyết vấn đề có liên quan:

  1. Sử dụng ($!) với return, sử dụng forceM trên số đầu tiên của catch, sử dụng catchStrict chức năng.
  2. Tôi cũng nhận thấy rằng đôi khi mọi người add some strictness cho các trường hợp máy biến áp của họ.

Dưới đây là một ví dụ:

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

Nó thực chất là the identity monad transformer, nhưng với nghiêm ngặt 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 

Trả lời

8

Thứ nhất (Tôi không chắc chắn nếu bạn biết điều này đã được), các lý do bắt không hoạt động với trường hợp lười biếng là

1 `div` 0 

biểu thức không được đánh giá cho đến khi cần, bên trong hàm print. Tuy nhiên, phương thức catch chỉ được áp dụng cho biểu thức f 0, không phải toàn bộ biểu thức print =<< f 0, do đó ngoại lệ không bị bắt. Nếu bạn đã làm:

test f = (print =<< f 0) `catch` \(_ :: SomeException) -> print 42 

thay vào đó, nó hoạt động chính xác trong cả hai trường hợp.

Nếu bạn muốn thực hiện một lệnh catch dù buộc đánh giá đầy đủ về kết quả IO, thay vì làm một trường hợp mới của NFData, bạn có thể viết một phương pháp forceM, và sử dụng rằng trong catchStrict phương pháp:

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`) 

(tôi là một chút ngạc nhiên rằng forceM không phải là bên trong thư viện Control.DeepSeq)


về nhận xét của bạn:

Không, quy tắc là ngoại lệ chỉ được ném khi giá trị được tính toán và điều đó chỉ được thực hiện khi cần thiết bởi haskell. Và nếu haskell có thể trì hoãn việc đánh giá một cái gì đó nó sẽ.

Một chức năng ví dụ kiểm tra mà không sử dụng $!, nhưng vẫn gây ra một ngoại lệ ngay lập tức (vì vậy bắt bình thường sẽ bắt chia cho số không ngoại lệ) là:

fooEvaluated :: Int -> IO Int 
fooEvaluated m = case 3 `div` m of 
    3 -> return 3 
    0 -> return 0 
    _ -> return 1 

Haskell buộc phải đánh giá Biểu thức "3` div` m ", vì nó cần khớp với kết quả ngược với 3 và 0.

Ví dụ cuối cùng, sau đây không ném bất kỳ ngoại lệ nào và khi được sử dụng với hàm thử trả về 1:

fooNoException :: Int -> IO Int 
fooNoException m = case 3 `div` m of 
    _ -> return 1 

Điều này là do haskell không bao giờ cần phải tính toán "3` div` m "biểu thức (như _ phù hợp với tất cả mọi thứ), do đó, nó không bao giờ được tính toán, do đó không có ngoại lệ được ném.

+0

Vì vậy, quy tắc là tôi cần phải vượt qua hành động IO thực (chẳng hạn như 'in') đối với hàm' catch', không phải 'trả về' cho một số giá trị thuần túy? – JJJ

+0

@ht .: Tôi đã thêm nhiều câu trả lời của mình để giải thích khi giá trị được đánh giá. –

+0

OK, cả hai ví dụ đều hoạt động tốt, 'fooEvaluated' cần đánh giá phân chia cho khớp mẫu, do đó ngoại lệ được ném và xử lý với hành động tùy chỉnh (' return 42' trong ví dụ của tôi), 'fooNoException' không cần phân chia tất cả và chỉ trả về '1', nó giống như' fooNoException m = const (return 1) (trả về (3 div m) :: IO Int) '. Khi tôi hỏi về quy tắc, tôi có nghĩa là cách duy nhất để bỏ lỡ một ngoại lệ (để hành động tùy chỉnh không được thực hiện) là 'catch' trên' return' cho 'm div 0',' head [] ',' fromJust không có gì, bất kỳ chức năng một phần, vv Nó thực sự là cách duy nhất? – JJJ

Các vấn đề liên quan