Search code examples
haskellbifunctor

Why doesn't `first` from Data.Bifunctor transform this value


In the following:

import Data.Bifunctor
import qualified Data.ByteString.Lazy.UTF8 as BLU

safeReadFile :: FilePath -> ExceptT Text IO Text
safeReadFile p = (lift $ doesFileExist p) >>= bool (throwError "File does not exist") (lift $ pack <$> readFile p)

safeDecodeJSONFile :: FromJSON a => Text -> FilePath -> ExceptT Text IO a
safeDecodeJSONFile t f = do
  contents <- safeReadFile f
  tryRight $ first (\x -> pack (x ++ (unpack t))) (eitherDecode (BLU.fromString (unpack contents)))

When I run runExceptT $ safeDecodeJSONFile "something" "nonExistantFile.json" I expect to get Left "Does not exist something" but instead I just get Left "Does not exist" - I know that the function I pass to first is being executed, since without the pack GHC complains that the type of (eitherDecode (BLU.fromString (unpack contents))) is ExceptT String IO a instead of ExceptT Text IO a - so why doesn't the concatenation from ++ also happen?


Solution

  • You've written

    safeDecodeJSONFile t f = do
      contents <- safeReadFile f
      tryRight $ ...
    

    The Monad instance for ExceptT gives up as soon as it hits Left, returning exactly that. So the tryRight ... never happens. You need to handle the Left case explicitly, perhaps using catchError.

    While we're at it, there's still a problem. You write

    safeReadFile :: FilePath -> ExceptT Text IO Text
    safeReadFile p = (lift $ doesFileExist p) >>= bool (throwError "File does not exist") (lift $ pack <$> readFile p)
    

    Unfortunately, this isn't reliable. First off, the file not existing is only one reason reading it can fail--there could be permission errors, network problems for networked filesystems, device errors if the file isn't a regular file, etc. Second, someone else could delete the file between the time you check its existence and the time you try to read it. The usual advice when trying to deal with files is not to check first. Just read the file and catch any exceptions using catch or similar in Control.Exception or wrappers around them