[RFC/WIP] class Example arg a
instead of an associated type
#749
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This PR is more of a discussion piece - I'd like to test it more thoroughly in an app before suggesting it for real integration, since it is a substantive API change with unforeseen consequenes.
MultiParamTypeClasses
vsTypeFamilies
The current
Example
class is defined with an associated type familyArg a
which determines the environment or context that you can use to test ana
.This is used for a few purposes:
IO
it msg \ () -> True
for non-function examplesThe
it
function handily shows how this is used:With the
instance Example Bool where type Arg Bool = ()
, we can fill this in:Making it a function (
instance Example (a -> Bool) where type Arg (a -> Bool) = a
, we can see how it expands out to accepting a function:As a result, we can write an additional case, as a specialization of the prior:
I'm gonna claim that the type family is overly restrictive, and that it'd be better to have a second parameter. The big reason is that we can be polymorphic in the
Arg
type, and the type family forbids that. For example, if we're testing aBool
, we really don't depend on theenv
at all. So we should be able to test like this:Polymorphic
arg
With this formulation:
Let's compare the instance implementations:
With the associated type, we can write
it
in any environment and provide aBool
and it "just works." This, to some extent, means that we don't even need to run the function to provide the value - so a pure test can be run very quickly and bypass anybefore_
that aren't useful for it!Testing a Monad
So let's say you've got a few different monads -
YesodExample
,AppT
, andDB
.YesodExample
needs anAppEnv
and aMiddleware
,AppT
needs anAppEnv
only, andDB
only requires aSqlBackend
. Note that an(AppEnv, Middleware)
contains all of these things (AppEnv
has aSqlBackend
inside). So we can, with the type family approach, express this:However, this approach means that you can't write
DB
andYesodExample
in the same block- if you have a BigwithYesodExample :: SpecWith (AppEnv, Middleware) -> Spec
, then you need to decompose that manually withbefore_
.You can instead choose to just have the
Arg
for each of these be hte maximal requirement, but that's unfortunate - it means we can't have special faster tests forDB
only.With the multi param type class, we can use
Has
-style constraints to make this work nicely.Now these can be seamlessly combined.
Function Examples
The current hooks API allows you to provide values to test examples. These are accessed with a function instance:
With the multiparam type class, this is expressed as so:
However, this has bad type inference properties: a much nicer approach uses the type equality trick.
But a natural extension to this is to require subtyping instead of equality. So we can also write:
Now we can test an
a -> Bool
in aSpecWith a
, orSpecWith (a, b)
.What would be especially interesting is if we could also generalize the delegation and have a single function instance. Consider:
Unfortunately, this doesn't play nicely with type inference - a polymorphic function now doesn't play nicely as an input without a bunch of type annotations. However, many functions that are polymorphic simply don't deal with their argument:
it msg \_ -> do ...
can now simply beit msg do ...
.There's one further behavior change with this function instance: the action is run once for
QuickCheck
properties, instead of multiple times.This test is the only failing test, once types are specified.
We can even write this:
And, with the function instance, we no longer need to worry about ever writing the boilerplate function instances again.