HMock
HMock copied to clipboard
Effect system integrations
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
, andeff
. - API layers, like
haxl
andservant
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.
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.
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
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.