HMock icon indicating copy to clipboard operation
HMock copied to clipboard

Effect system integrations

Open cdsmith opened this issue 3 years ago • 3 comments

Migrated from TODO file.

There are a lot of libraries in Haskell that can be used to define an API to some external system. Ideally, there would be a way to mock all of these with HMock. This includes:

  • Effect systems like polysemy, fused-effects, freer-simple, and eff.
  • API layers, like haxl and servant

This needs some serious thought about how to separate mtl-isms from the more reusable core of HMock, so that as much as reasonable can be shared with other systems. I suppose the right way to go about this is to take a few examples, ask how you'd reimplement HMock for that specific system, and then look for the right refactorings to share the common bits. The HMock core expectation language, surface expectation language, and most of the MockableBase class should hopefully be shared.

Many of these systems already have their own action types, which could be trivially wrapped rather than deriving a new Action class. This might mean that Action should be an injective type family rather than a data family. However, we might need to lose some type safety for this to occur, since the method name isn't encoded into the type any longer. This definitely might interfere with any type tricks of the sort I'm contemplating to implement polymorphic return values, too.

cdsmith avatar Jun 30 '21 01:06 cdsmith

Since I've split servant and haxl into their own issues, I'm claiming this issue for effect system integrations. This includes:

  • polysemy
  • fused-effects
  • freer-simple
  • eff

and perhaps others.

cdsmith avatar Sep 08 '21 17:09 cdsmith

I imagine this will eventually look something like the following:

class MonadFoo where
    mtlFoo :: Int -> m ()

data FooEffect m a where
    PolysemyFoo :: Int -> FooEffect m ()

data EffectSystem = MTLStyle | Polysemy | FusedEffects | Eff | ...

type family EffectType ... where
  EffectType MTLStyle = (Type -> Type) -> Constraint
  EffectType Polysemy = (Type -> Type) -> (Type -> Type)

class Mockable (sys :: EffectSystem) (eff :: EffectType sys) | eff -> sys where ...

-- Instances, usually written by Template Haskell
instance MockableBase MTLStyle MonadFoo where
  data Action MTLStyle MonadFoo (name :: Symbol) (m :: Type -> Type) (a :: Type) where
    MtlFoo :: Int -> Action MTLStyle MonadFoo "mtlFoo" m ()

  data Matcher MTLStyle MonadFoo (name :: Symbol) (m :: Type -> Type) (a :: Type) where
    MtlFoo_ :: Predicate Int -> Matcher MTLStyle MonadFoo "mtlFoo" m ()

  ...

instance MockableBase Polysemy FooEffect where
  data Action Polysemy FooEffect ...
    -- open question: reuse the GADT FooEffect?  Or define a new Action GADT?
    -- Advantage to #1: Seems more natural for polysemy.  Fewer name conflicts.
    -- Advantage to #2: We get the operation name in the type for type-safety.

  data Matcher Polysemy FooEffect ... where
    PolysemyFoo_ :: Predicate Int -> Matcher Polysemy FooEffect ...

-- Template Haskell generators can just take the effect system as a value-level argument.
makeMockable MTLStyle [t| MonadFoo |]
makeMockable Polysemy [t| FooEffect |]

-- Runner for MTLStyle
instance MonadFoo MockT where ...

-- Runner for polysemy - can this be polymorphic in the effect?  Probably not.
mockFooEffect :: Has MockEffect r => Sem (FooEffect : r) a -> Sem r a
interpretMocks :: ... => Sem (MockEffect : r) a -> Sem r a

cdsmith avatar Sep 08 '21 18:09 cdsmith

There are currently a lot of dependency circles involving MockT in HMock's implementation, so the real trick here is going to be finding the best way to disentangle all of this stuff that shares as much of the test writer experience as possible while still working for different effect systems. In particular, we need to be able to share the expectation combinators: expect, expectN, expectAny, inSequence, and so on. Since these depend on Rule, this probably means that Rule needs to incorporate a type that depends on the effect system, as well.

I have deliberately left HMock in a pre-1.0 version number, and warned people to use upper bounds on dependencies, in anticipation of needing to make significant changes so that this will work.

cdsmith avatar Sep 08 '21 18:09 cdsmith