Search code examples
haskellexceptioniofilesystems

Exception constraint in type class in Haskell


I have a problem. I am trying to write down a class type for File system management. I started with a simple class:

import Control.Exception

class (Monad m) => Commands m where

  --Similar to catch :: Exception e => IO a -> (e -> IO a) -> IO a
  fsCatch
    :: Exception e
    => m a
    -> (e -> m a)
    -> m a

  setCurDir
    :: FilePath
    -> m ()

  caughtError
    :: Exception e
    => e
    -> m ()
  cd
    :: FilePath
    -> m ()
  cd path = fsCatch (setCurDir path) caughtError

There is function cd (to move over directories). It tries to change directory with setCurDir (need to implement) and if something goes wrong it invokes caughtError function.

I want to make two instances: for IO and my 'toy' FileSystem data class.

For IO it is going to look something like this:

instance Commands IO where
  fsCatch = catch

  setCurDir = setCurrentDirectory

  caughtError ex | isDoesNotExistError ex = putStrLn "No such directory"
                 | etc...

IO has its own IOError. And I am going to make MyFSError for my toy FileSystem. But I got this error in my type class:

 * Could not deduce (Exception e0) arising from a use of `fsCatch'
      from the context: Commands m
        bound by the class declaration for `Commands'
        at D:\\University\FP\hw3-olegggatttor\hw3\src\Commands.hs:6:20-27
      The type variable `e0' is ambiguous
      These potential instances exist:
        instance Exception ErrorCall -- Defined in `GHC.Exception'
        instance Exception ArithException
          -- Defined in `GHC.Exception.Type'
        instance Exception SomeException -- Defined in `GHC.Exception.Type'
        ...plus 10 others
        ...plus three instances involving out-of-scope types
        (use -fprint-potential-instances to see them all)
    * In the expression: fsCatch (setCurDir path) catchedError
      In an equation for `cd':
          cd path = fsCatch (setCurDir path) catchedError
   |
61 |   cd path = fsCatch (setCurDir path) catchedError
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Failed, no modules loaded.

Where is the problem and how to fix it? It is necessary to use class type.


Solution

  • The problem is that cd needs an Exception e constraint to call fsCatch and caughtError. However, there's nothing in the definition of cd or in its type signature that would provide such a constraint. Heck, the type e isn't even mentioned in the type signature for cd.

    For this particular type class, it looks like you want the monad m to determine the exception type e. If m ~ IO, then e ~ IOError, and if m ~ ToyFilesystem, then e ~ MyFSError. This can be accomplished using either functional dependencies or associated type families. I think the consensus is probably that the associated type families approach is the more modern, straightforward one and so should be preferred, but I'll show you both.

    For a functional dependencies implementation, you reparametrize the type class to include the exception type e, and add an annotation m -> e that indicates that the type of the monad uniquely determines the type of the exception:

    {-# LANGUAGE FunctionalDependencies #-}
    {-# LANGUAGE MultiParamTypeClasses #-}
    
    class (Monad m, Exception e) => Commands m e | m -> e where
      fsCatch :: m a -> (e -> m a) -> m a
      setCurDir :: FilePath -> m ()
      caughtError :: e -> m ()
      cd :: FilePath -> m ()
      cd path = fsCatch (setCurDir path) caughtError
    

    and the IO instance looks like:

    instance Commands IO IOError where
      fsCatch = catch
      setCurDir = setCurrentDirectory
      caughtError ex | isDoesNotExistError ex = putStrLn "No such directory"
    

    Note that you don't actually need the Monad m or Exception e constraints in this class at all, so switching the class declaration to:

    class Commands m e | m -> e where
      ...
    

    works fine. I would argue that this is better practice -- that no Monad or Exception constraints should appear anywhere in your class declaration -- but that's an answer for another SO question.

    For an associated type families implementation, you add a type family declaration to the class that explicitly maps the monad to its exception type:

    class (Monad m, Exception (E m)) => Commands m where
      type E m  -- exception type for monad m
      fsCatch :: m a -> (E m -> m a) -> m a
      setCurDir :: FilePath -> m ()
      caughtError :: E m -> m ()
      cd :: FilePath -> m ()
      cd path = fsCatch (setCurDir path) caughtError
    

    Again, the constraints aren't needed here, and you could write:

    class Commands m where
      ...
    

    The instances then specify the the type E m for the monad m:

    instance Commands IO where
      type E IO = IOError
      fsCatch = catch
      setCurDir = setCurrentDirectory
      caughtError ex | isDoesNotExistError ex = putStrLn "No such directory"
    

    The full examples, for functional dependencies:

    {-# LANGUAGE FunctionalDependencies #-}
    {-# LANGUAGE MultiParamTypeClasses #-}
    {-# LANGUAGE TypeSynonymInstances #-}
    
    import Control.Exception
    import System.Directory
    import System.IO.Error
    
    class Commands m e | m -> e where
      fsCatch :: m a -> (e -> m a) -> m a
      setCurDir :: FilePath -> m ()
      caughtError :: e -> m ()
      cd :: FilePath -> m ()
      cd path = fsCatch (setCurDir path) caughtError
    
    instance Commands IO IOError where
      fsCatch = catch
      setCurDir = setCurrentDirectory
      caughtError ex | isDoesNotExistError ex = putStrLn "No such directory"
    

    and for associated type families:

    {-# LANGUAGE FlexibleContexts #-}
    {-# LANGUAGE TypeFamilies #-}
    
    import Control.Exception
    import System.Directory
    import System.IO.Error
    
    class Commands m where
      type E m
      fsCatch :: m a -> (E m -> m a) -> m a
      setCurDir :: FilePath -> m ()
      caughtError :: E m -> m ()
      cd :: FilePath -> m ()
      cd path = fsCatch (setCurDir path) caughtError
    
    instance Commands IO where
      type E IO = IOError
      fsCatch = catch
      setCurDir = setCurrentDirectory
      caughtError ex | isDoesNotExistError ex = putStrLn "No such directory"