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
= IO.hIsEOF
hIsEOF = IO.withFile 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
= withFile path ReadMode hIsEOF isFileEmpty path
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 ()
= defaultMain . testCase "experiment" $ do
main <- runMockT $ do
isEmpty $ WithFile_ anything
expect |=> \(WithFile "test.txt" ReadMode _action) -> pure True
"test.txt"
isFileEmpty 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 ()
= defaultMain . testCase "experiment" $ do
main <- runMockT $ do
result $ ExpWithF_ anything
expect |=> \(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 ()
= defaultMain . testCase "experiment" $ do
main <- runMockT $ do
result $ ExpWithF_ anything
expect |=> \(ExpWithF "one.txt" _action) -> pure "1"
"one.txt" expShowInt
expWithF "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.