Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Absolutely forcing a catch of an Error in Haskell

I am using the hs-excelx library that itself uses zip-archive. zip-archive is reaching a condition in which it calls fail, which in that particular context, evaluates to a call to error. This is a call to error in pure code.

I'm trying to detect whether a particular file is actually an Excel file. It's actually necessary for me to detect this without crashing, so I have written a function called isExcel to do the detection:

import qualified Data.Excelx as E

isExcel :: BS.ByteString -> Bool
isExcel = maybe False (\_ -> True) . E.toExcelx

Now, the catch is that this is a mere formality. If you call E.toExcelx on a bytestring that is not a zip archive, zip-archive will simply error out.

But, I know that I'm calling isExcel in IO code, so I wrote an IO function to try to catch the error like this:

import qualified Data.ByteString.Lazy as BS
import Control.Exception

sd :: BS.ByteString -> IO Bool
sd bs = handle handler $ do
        ie <- return $ Excel.isExcel bs
        return (ie `seq` ie)
    where
    handler :: SomeException -> IO Bool
    handler e = return False

> sd BS.empty
*** Exception: too few bytes. Failed reading at byte position 4

What is going on? According to what I've read at http://www.haskell.org/haskellwiki/Error_vs._Exception and other places, I should be catching the exception and converting it to something useful. ie in my code might be a thunk to a Bool, but how can running seq on ie possibly leave anything unevaluated? How can I possibly catch an exception like this when I can't even figure out how to force the evaluation of a value? I don't have time to go in and hack proper error handling into zip-archive.

like image 296
Savanni D'Gerinel Avatar asked Mar 21 '23 23:03

Savanni D'Gerinel


1 Answers

There are few things going wrong here. First, to force the evaluation of a value in IO, use Control.Exception.evaluate. This function has the type evaluate :: a -> IO a, and it's basically a hook to tell the compiler to force an evaluation. Its sole reason for existence is to enable exception handling.

Next, you need some machinery to actually catch the exception. You're currently using handle, but Control.Exception.try, which has the type try :: Exception e => IO a -> IO (Either e a), is probably a bit simpler to use here. This type is slightly odd, but it means that, for some exception type you specify, it try will either return the value or an exception. If evaluation threw an exception that isn't the type you specified (and can't be coerced to it), try will re-throw the exception.

Calls to error produce an exception of the type ErrorCall, so to handle the exception you could use

sd :: BS.ByteString -> IO Bool
sd bs = do
        ie <- try $ evaluate $ Excel.isExcel bs
        either (const False) (id) (ie :: Either ErrorCall Bool)

of course you know that isExcel will only return True. You could use toExcel directly, and modify the either line to accommodate that.

As to the source of your problem,

a `seq` a

is exactly equivalent to a. It means, "at the time you evaluate (the second) a, evaluate (the first) a too". In other words, it's still too lazy. That's why you need evaluate.

like image 141
John L Avatar answered Apr 08 '23 12:04

John L