Skip to main content

HMock and Impredicative Polymorphism

I have not been successful with getting HMock to work with functions like withFile. I keep getting impredicative polymorphism errors.

The withFile function opens a file using the specified mode and passes the file handle to a function that returns a result. It ensures that the file handle is closed, even in cases that an exception is thrown. Note that the value of the return type is determined by the return type of the function passed as an argument.

withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r

For a minimal test, consider the following type class. Note that the Typeable constraint is added because it is required by HMock.

class Monad m => MonadHandle m where
  hIsEOF :: Handle -> m Bool
  withFile :: Typeable r => FilePath -> IOMode -> (Handle -> m r) -> m r

The functions of the same names in System.IO are used as the IO instances.

instance MonadHandle IO where
  hIsEOF = IO.hIsEOF
  withFile = IO.withFile

The following test function checks if a file is empty by calling the hIsEOF function on the file handle passed by withFile.

isFileEmpty :: MonadHandle m => FilePath -> m Bool
isFileEmpty path = withFile path ReadMode hIsEOF

HMock instances can be created using the makeMockable function without issue.

makeMockable [t|MonadHandle|]

Attempting to create a rule fails, however. The intention of the rule in the following code is to match the first two arguments to a call of withFile, ignoring the third (function) argument, and return True.

main :: IO ()
main = defaultMain . testCase "experiment" $ do
    isEmpty <- runMockT $ do
      expect $ WithFile_ anything
        |=> \(WithFile "test.txt" ReadMode _action) -> pure True
      isFileEmpty "test.txt"
    True @=? isEmpty

Compilation fails with two errors. The first error is the impredicative polymorphism error.

Experiment.hs:57:16: error:
    • Cannot instantiate unification variable ‘ex0’
      with a type involving polytypes:
        Test.Predicates.Predicate IOMode
        -> (forall r.
            Typeable r =>
            Test.Predicates.Predicate
              (Handle -> Test.HMock.Internal.State.MockT b0 r))
        -> Test.HMock.Mockable.Matcher MonadHandle "withFile" b0 c0
        GHC doesn't yet support impredicative polymorphism
    • In the first argument of ‘(|=>)’, namely ‘WithFile_ anything’
      In the second argument of ‘($)’, namely
        ‘WithFile_ anything
           |=> \ (WithFile "test.txt" ReadMode _action) -> pure True’
      In a stmt of a 'do' block:
        expect
          $ WithFile_ anything
              |=> \ (WithFile "test.txt" ReadMode _action) -> pure True
   |
57 |       expect $ WithFile_ anything
   |                ^^^^^^^^^^^^^^^^^^

The second error is an “untouchable” error concerning unification of the return value type.

Experiment.hs:58:56: error:
    • Couldn't match type ‘r0’ with ‘Bool’
        ‘r0’ is untouchable
          inside the constraints: (name0 ~ "withFile", Typeable r0)
          bound by a pattern with constructor:
                     WithFile :: forall c (b :: * -> *).
                                 Typeable c =>
                                 FilePath
                                 -> IOMode
                                 -> (Handle -> Test.HMock.Internal.State.MockT b c)
                                 -> Test.HMock.Mockable.Action MonadHandle "withFile" b c,
                   in a lambda abstraction
          at Experiment.hs:58:15-50
      Expected type: Test.HMock.Internal.State.MockT IO r0
        Actual type: Test.HMock.Internal.State.MockT IO Bool
    • In the expression: pure True
      In the second argument of ‘(|=>)’, namely
        ‘\ (WithFile "test.txt" ReadMode _action) -> pure True’
      In the second argument of ‘($)’, namely
        ‘WithFile_ anything
           |=> \ (WithFile "test.txt" ReadMode _action) -> pure True’
    • Relevant bindings include
        _action :: Handle -> Test.HMock.Internal.State.MockT IO r0
          (bound at Experiment.hs:58:44)
   |
58 |         |=> \(WithFile "test.txt" ReadMode _action) -> pure True
   |                                                        ^^^^^^^^^

I experimented with the test code to search for a minimal change that causes this failure. The return value of the expWithF function in the following type class is determined by the return value of the passed function, like withFile, but it does not have any other parameters.

class Monad m => MonadExp m where
  expShowInt :: Int -> m String
  expWithF :: Typeable r => (Int -> m r) -> m r

makeMockable [t|MonadExp|]

The following test compiles and runs without error.

main :: IO ()
main = defaultMain . testCase "experiment" $ do
    result <- runMockT $ do
      expect $ ExpWithF_ anything
        |=> \(ExpWithF _action) -> pure "1"
      expWithF expShowInt
    "1" @=? result

Adding a parameter causes the compilation to fail with the above errors.

class Monad m => MonadExp m where
  expShowInt :: Int -> m String
  expWithF :: Typeable r => FilePath -> (Int -> m r) -> m r

makeMockable [t|MonadExp|]

main :: IO ()
main = defaultMain . testCase "experiment" $ do
    result <- runMockT $ do
      expect $ ExpWithF_ anything
        |=> \(ExpWithF "one.txt" _action) -> pure "1"
      expWithF "one.txt" expShowInt
    "1" @=? result

Is there a way to mock functions like withFile using HMock? I eventually resolved the issue! See HMock and Impredicative Polymorphism (Part 2) for details.

The above code is available on GitHub.