2012-06-22 2 views
9

Die Frage ist ähnlich this Frage. In diesem Fall geht es jedoch um Ausnahmen, nicht um faule E/A. HierWie funktionieren Faulheit und Ausnahmen in Haskell zusammen?

ist ein Test:

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

Also schrieb ich zwei Funktionen fooLazy, die faul und fooStrict ist, die streng ist, auch gibt es zwei Tests testLazy und testStrict, dann versuche ich Division durch Null zu fangen:

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

und es scheitert in faulen Fällen.

Das erste, was ist in den Sinn kommt eine Version der catch Funktion zu schreiben, die die Bewertung auf das erste Argument erzwingen:

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

es scheint zu funktionieren:

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

aber ich Verwenden Sie die unsafePerformIO Funktion hier und das ist gruselig.

Ich habe zwei Fragen:

  1. kann man sicher sein, dass die catch Funktion immer alle Ausnahmen abfängt, und zwar unabhängig von der Art des es erstes Argumente?
  2. Wenn nicht, gibt es einen bekannten Weg, um mit dieser Art von Problemen umzugehen? So etwas wie die catchStrict Funktion ist geeignet?

UPDATE 1.

Dies ist eine bessere Version der catchStrict Funktion von 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. Hier

ist eine andere 'schlechte' Beispiel:

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

Es sollte wie folgt neu geschrieben werden:

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

UPDATE 3.

Wie nanothief bemerkt, gibt es keine Garantie, dass die catch Funktion immer irgendeine Ausnahme abfangen. Also muss man es vorsichtig benutzen.

Ein paar Tipps, wie die damit verbundenen Probleme zu lösen:

  1. Verwenden ($!) mit return verwenden forceM auf das erste Argument von catch, verwenden Sie die catchStrict Funktion.
  2. Ich bemerkte auch, dass manchmal Menschen add some strictness zu Instanzen ihrer Transformatoren.

Hier ein Beispiel:

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

Es ist im Wesentlichen the identity monad transformer, aber mit strengen 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 

Antwort

8

Zum einen (Ich bin nicht sicher, ob Sie das schon wissen), die Grund der Fang funktioniert nicht mit dem Lazy Case ist die

1 `div` 0 

Der Ausdruck wird erst ausgewertet, wenn er benötigt wird, und zwar innerhalb der print-Funktion. Die Methode catch wird jedoch nur auf den Ausdruck f 0 angewendet, nicht auf den gesamten Ausdruck print =<< f 0, sodass die Ausnahme nicht abgefangen wird. Wenn Sie:

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

stattdessen funktioniert es in beiden Fällen korrekt.

Wenn Sie einen Haken Aussage machen wollen allerdings, dass eine vollständige Bewertung des Ergebnisses IO zwingt, anstatt eine neue Instanz von NFData machen, könnten Sie eine forceM Methode schreiben, und verwenden, die in der catchStrict Methode:

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

(ich bin ein wenig überrascht, dass FORCEM nicht innerhalb der Control.DeepSeq Bibliothek ist)


In Bezug auf Ihren Kommentar:

Nein, die Regel ist die Ausnahme wird nur ausgelöst, wenn der Wert berechnet wird, und das wird nur gemacht, wenn es von Haskell benötigt wird. Und wenn Haskell die Bewertung von etwas verzögern kann, wird es das tun.

Ein Beispiel Testfunktion, die nicht $! nicht verwendet, sondern verursacht immer noch eine Ausnahme sofort (so die normale fangen wird die Division durch Null Ausnahme abfangen) ist:

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

Haskell auszuwertenden gezwungen ist, die "3` div` m "Ausdruck, wie es das Ergebnis gegen 3 und 0 übereinstimmen muss.

Als letztes Beispiel löst das folgende keine Ausnahme aus, und wenn es mit der Testfunktion verwandt wird, gibt 1:

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

Dies ist, weil harkell nie "3` div` m "Ausdruck berechnen muss (wie _ alles passt), so dass es nie berechnet wird, daher keine Ausnahme geworfen wird.

+0

Also ist die Regel, dass ich echte IO-Aktion (wie "print") an die "catch" -Funktion übergeben muss, nicht die "Rückkehr" für einige reine Wert? – JJJ

+0

@ht .: Ich habe meiner Antwort mehr hinzugefügt, um zu erklären, wann die Werte ausgewertet werden. –

+0

OK, beide Beispiele funktionieren gut, 'fooEvaluated' muss Division für Mustervergleich auswerten, so dass die Ausnahme ausgelöst und mit einer benutzerdefinierten Aktion behandelt wird (' return42' in meinem Beispiel), 'fooNoException' braucht keine Division bei alles und gibt nur '1 'zurück, es ist dasselbe wie' fooNoException m = const (Rückkehr 1) (Rückkehr (3 div m) :: IO Int) '. Wenn ich nach der Regel frage, meine ich, dass die einzige Möglichkeit, eine Ausnahme zu verpassen (so dass eine benutzerdefinierte Aktion nicht ausgeführt wird), '' '' return 'für 'm div 0',' head [] ',' fromJust nichts, irgendeine Teilfunktion, etc. Es ist wirklich der einzige Weg? – JJJ