GHC is saying my function is too general to be passed as an argument.
Here is a simplified version that reproduces the error:
data Action m a = SomeAction (m a)
runAction :: Action m a -> m a
runAction (SomeAction ma) = ma
-- Errors in here
actionFile :: (Action IO a -> IO a) -> String -> IO ()
actionFile actionFunc fileName = do
actionFunc $ SomeAction $ readFile fileName
actionFunc $ SomeAction $ putStrLn fileName
main :: IO ()
main =
actionFile runAction "Some Name.txt"
This is what the error says:
• Couldn't match type ‘a’ with ‘()’
‘a’ is a rigid type variable bound by
the type signature for:
actionFile :: forall a. (Action IO a -> IO a) -> String -> IO ()
at src/Lib.hs:11:15
Expected type: Action IO a
Actual type: Action IO ()
The compiler wants me to be more specific in my type signature, but I can't because I will need to use the parameter function with different types of arguments. Just like in my example I pass it an Action IO ()
and an Action IO String
.
If I substitute (Action IO a -> IO a) -> String -> IO ()
for (Action IO () -> IO ()) -> String -> IO ()
, like the compiler asked, the invocation with readFile
errors because it outputs an IO String
.
Why is this happening and what should I do to be able to pass this function as an argument?
I know that if I just use runAction
inside my actionFile
function everything will work, but in my real code runAction
is a partially applied function that gets built from results of IO computations, so it is not available at compile time.
This is a quantifier problem. The type
actionFile :: (Action IO a -> IO a) -> String -> IO ()
means, as reported by the GHC error,
actionFile :: forall a. (Action IO a -> IO a) -> String -> IO ()
which states the following:
a
g :: Action IO a -> IO a
String
actionFile
must answer with an IO ()
Note that a
is chosen by the caller, not by actionFile
. From the point of view of actionFile
, such type variable is bound to a fixed unknown type, chosen by someone else: this is the "rigid" type variable GHC mentions in the error.
However, actionFile
is calling g
passing an Action IO ()
argument (because of putStrLn
). This means that actionFile
wants to choose a = ()
. Since the caller can choose a different a
, a type error is raised.
Further, actionFile
also wants to call g
passing an Action IO String
argument (because of readFile
), so we also want to choose a = String
. This implies that g
must accept the choice of whatever a
we wish.
As mentioned by Alexis King, a solution could be to move the quantifier and use a rank-2 type:
actionFile :: (forall a. Action IO a -> IO a) -> String -> IO ()
This new type means that:
g :: forall a. Action IO a -> IO a
g
(i.e., actionFile
) must choose a
g
(i.e., actionFile
) must provide an Action IO a
g
must provide an IO a
String
actionFile
must answer with an IO ()
This makes it possible to actionFile
to choose a
as wanted.