Skip to content
This repository has been archived by the owner on Jun 18, 2021. It is now read-only.

Factor out hfunctor and friends into its own package #170

Closed
stevana opened this issue Oct 9, 2017 · 12 comments
Closed

Factor out hfunctor and friends into its own package #170

stevana opened this issue Oct 9, 2017 · 12 comments

Comments

@stevana
Copy link
Collaborator

stevana commented Oct 9, 2017

As per discussion in #161.

@Lysxia
Copy link
Collaborator

Lysxia commented Oct 9, 2017

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 constraint forall a. HFunctor' (Action a).

  • Wait for Quantified contexts to land (who knows when). (GHC issue, ICFP'17 paper)

@stevana
Copy link
Collaborator Author

stevana commented Oct 10, 2017

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?

@jacobstanley
Copy link

jacobstanley commented Oct 15, 2017

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

@Lysxia
Copy link
Collaborator

Lysxia commented Oct 16, 2017

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 hides ForallF from the main signatures, and also gives us a good place to document how that works for users who wonder about the mismatch between the HTraversable they write vs the HTraversable1 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 write HTraversable1 (and HFoldable1 and HFunctor1 if they're still around) as they do currently.

@jacobstanley
Copy link

Now that you explain it, both solution sound pretty reasonable, especially if the hard work is hidden behind TH. Do you have a preference?

@Lysxia
Copy link
Collaborator

Lysxia commented Oct 17, 2017

I prefer ForallF.

@stevana
Copy link
Collaborator Author

stevana commented Oct 17, 2017

(I'm fine with either.)

@jacobstanley
Copy link

I'm ok with ForallF, especially if we can use a type synonym

@stevana
Copy link
Collaborator Author

stevana commented Oct 24, 2017

Another thing worth considering: how much does the GADT give us? Could it be worth scraping it, so we can use Generics 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?

@Lysxia
Copy link
Collaborator

Lysxia commented Oct 27, 2017

I just found that the rank2classes has the higher-order Traversable already. But @stevana you raise a good point.

@stevana
Copy link
Collaborator Author

stevana commented Feb 12, 2018

@stevana
Copy link
Collaborator Author

stevana commented Aug 20, 2018

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

@stevana stevana closed this as completed Aug 20, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants