Search code examples
haskelltypeshindley-milner

Haskell: Rigid type variable error when passing function as argument


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.


Solution

  • 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:

    • the caller must choose a type a
    • the caller must provide a function g :: Action IO a -> IO a
    • the caller must provide a String
    • finally, 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:

    • the caller must provide a function g :: forall a. Action IO a -> IO a
      • the caller of g (i.e., actionFile) must choose a
      • the caller of g (i.e., actionFile) must provide an Action IO a
      • finally, g must provide an IO a
    • the caller must provide a String
    • finally, actionFile must answer with an IO ()

    This makes it possible to actionFile to choose a as wanted.