2

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.

Marcelo Lazaroni
  • 9,819
  • 3
  • 35
  • 41
  • 5
    You want a rank 2 type, but standard Haskell only permits rank 1 types. Enable the `RankNTypes` extension and change the type of `actionFile` to `(forall a. Action IO a -> IO a) -> String -> IO ()`. – Alexis King Jul 25 '17 at 07:24
  • Beautiful. It works. I will read more about type ranking to understand what's going on and what guarantees I may be giving up with this language extension. Thanks. – Marcelo Lazaroni Jul 25 '17 at 07:31

1 Answers1

6

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.

chi
  • 111,837
  • 3
  • 133
  • 218
  • Thanks for that. Do you know of any other solution that would not use a language extension? – Marcelo Lazaroni Jul 25 '17 at 20:38
  • 2
    @MarceloLazaroni Not real ones. You could make `actionFile : (Action IO () -> IO ()) ->(Action IO String -> IO String) -> String -> IO ()` but it would be weird. Also, note that it's is very common for modern libraries and applications to exploit many extensions. I'd even say nobody writes serious code in plain Haskell. Most of the extensions are harmless, and arguably a large number of them should just be included in a revised Haskell Report, just because they've become so popular. Don't be afraid to use them, everybody is already doing that. – chi Jul 25 '17 at 20:55