Factor out hfunctor and friends into its own package
stevana opened this issue · comments
Thinking about the abstraction behind HFunctor
.
Originally, HFunctor
is the class of functors between two functor categories (as it appears in a category-extras module for instance, now vanished from Hackage).
hfmap :: HFunctor t => (forall x. u x -> v x) -> (forall a. t u a -> t v a)
But hfmap
alone doesn't quite express the intent that t :: (* -> *) -> (* -> *)
maps functors to functors. We would also need something like forall u. Functor u => Functor (t u)
.
In quickcheck-state-machine, the user-defined type Action v
is not a functor, so Action
is not an HFunctor
in the sense above. I mean, the fact that we can implement and use HFunctor
for Action
is somewhat of a coincidence, thanks to a lack of expressiveness of the constraints language in Haskell.
So our HFunctor
abstraction is actually a bit ad-hoc, and I hope that we can decompose it into elements that are easier to reuse as part of their own package.
The variable a
is "second-class" in that there aren't interesting ways to map over it, and I would be tempted to change the order to Action a v
, making a more "natural" signature be:
hfmap' :: HFunctor' f => (forall x. u x -> v x) -> f u -> f v
This HFunctor'
represents functors between a functor category and Hask (f :: (* -> *) -> *
).
Note that hedgehog's HTraversable
follows that pattern for the kind of f
:
-- From hedgehog
htraverse :: (HTraversable f, Applicative m) => (forall x. u x -> m (v x)) -> f u -> m (f v)
To make the kinds match, we would need to write HFunctor' (Action a)
. However, the combinators in quickcheck-state-machine rely on a parametric instance, informally forall a. HFunctor' (Action a)
. Three ways to do it:
-
Create a new type class, an "indexed" variant of
HFunctor'
:hfmap1' :: HFunctor1' (t :: * -> (* -> *) -> *) => (forall x. u x -> v x) -> t a u -> t a v
The current way we are doing things can be seen as (ab)using
HFunctor
to play that role. -
Use
ForallF
from the constraints package:ForallF HFunctor' Action
represents the universally quantified constraintforall a. HFunctor' (Action a)
. -
Wait for Quantified contexts to land (who knows when). (GHC issue, ICFP'17 paper)
Neither choice is very satisfying. I guess the HFunctor1
would be nicer for us, while the ForallF
route would be better when taking Hedgehog into consideration (which we should).
The first release of quickcheck-state-machine
used to have ForallF
constraint in the types, and I think it made the types look more intimidating. I guess things are better now with the actual instances being hidden away with template Haskell, but still...
@jystic: Thoughts?
I would only say that I don't mind too much if there is extra cruft in the internal usage of the abstraction, but I would prefer that the way clients use the API is as clean as possible. As such the ForallF
approach, if I understand it correctly, isn't that satisfying.
@stevana in quickcheck-state-machine
's Action v a
, what does the a
represent?
Ignore me! I see now that's it's just got the arguments flipped compared with Var
in hedgehog
The user still writes almost the same code with ForallF
. It's just that the instance declaration changes from HTraversable Action
to HTraversable (Action a)
, and this becomes invisible if they use TH.
We could define a "quickcheck-state-machine"-specific HTraversable1
on top of a more general HTraversable
:
-
either as a type class synonym
HTraversable1 = ForallF HTraversable
, which hidesForallF
from the main signatures, and also gives us a good place to document how that works for users who wonder about the mismatch between theHTraversable
they write vs theHTraversable1
that's required of them; -
or we can avoid the Forall hack, with a custom type class and a default implementation.
class HTraversable1 t where htraverse1 :: Applicative m => (forall x. f x -> g x) -> t a f -> m (t a g) default htraverse1 :: (HTraversable (t a), Applicative m) => (forall x. f x -> g x) -> t a f -> t a g htraverse1 = htraverse
It's a bit more boilerplate, but users that are not interested in all the cruft with
HTraversable
can also choose to only writeHTraversable1
(andHFoldable1
andHFunctor1
if they're still around) as they do currently.
Now that you explain it, both solution sound pretty reasonable, especially if the hard work is hidden behind TH. Do you have a preference?
I prefer ForallF
.
(I'm fine with either.)
I'm ok with ForallF
, especially if we can use a type synonym
Another thing worth considering: how much does the GADT give us? Could it be worth scraping it, so we can use Generic
s rather than template Haskell? Or could we have both by having template Haskell derive an untyped (non-GADT) version of the action datatype (for which we have generics), and two convertion functions between the GADT and non-GADT version of actions?
I just found that the rank2classes has the higher-order Traversable
already. But @stevana you raise a good point.
We ended up going for the non-GADT approach together with a generic version of rank2classes in #209, see: https://github.com/advancedtelematic/quickcheck-state-machine/blob/master/src/Test/StateMachine/Types/Rank2.hs