Skip to main content

HMock and Impredicative Polymorphism (Part 2)

I reviewed the documentation on impredicative polymorphism yesterday while calming a tired baby, and I realized that I could probably resolve the issue described in the HMock and Impredicative Polymorphism blog entry by simply specifying types! I was able to try it out this morning, and I was indeed able to resolve the issue.

Following the types, I discovered an error in the test code. Here is the original code, for reference.

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

The WithFile_ function, created by makeMockable constructs a Matcher, which is a specification for matching actions. The following definition is displayed by dumping the Template Haskell splices.

data Test.HMock.Mockable.Matcher MonadHandle
    :: ghc-prim-0.6.1:GHC.Types.Symbol
    -> (ghc-prim-0.6.1:GHC.Types.Type -> ghc-prim-0.6.1:GHC.Types.Type)
    -> ghc-prim-0.6.1:GHC.Types.Type
    -> ghc-prim-0.6.1:GHC.Types.Type
  where
    HIsEOF_
      :: (Test.Predicates.Predicate Handle)
      -> Test.HMock.Mockable.Matcher MonadHandle "hIsEOF" m_a6hj Bool
    WithFile_
      :: (Test.Predicates.Predicate FilePath)
      -> (Test.Predicates.Predicate IOMode)
      -> ( forall r_a6lZ
         . Typeable r_a6lZ
         => Test.Predicates.Predicate
              (Handle -> Test.HMock.Internal.State.MockT m_a6hj r_a6lZ)
         )
      -> Test.HMock.Mockable.Matcher MonadHandle "withFile" m_a6hj r_a6lZ

A Matcher constructor of course takes a Predicate for each parameter of the mocked function! I had only seen examples with a single Predicate and only put one anything argument, but I need one for each argument.

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

For this minimal test, that change alone resolves the issue!

In my actual code, the return value is more complicated. The impredicative polymorphism error goes away, but I got a Typeable instance error instead. It seems that the constraint in withFile was lost, resolving one issue but causing another. This was easily solved by specifying the type of the Matcher.

expect $
  ( WithFile_ anything anything anything
      :: Matcher MonadHandle "withFile" IO ConcreteType
  )
  |=> \(WithFile "test.md" ReadMode _action) -> pure concreteValue

Note that the underlying monad is IO because that is the monad used by HUnit and Tasty.

The updated code is available on GitHub.