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.
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"