haskell-hedgehog
haskell-hedgehog copied to clipboard
Provide a free `Functor` or `Applicative` wrapper to enable pretend computations over `Symbolic` values
In #459, there have been a number of approaches proposed to solve the "make a Command
return multiple values" problem. @ocharles used a GADT to build a defunctionalised expression tree with the specific functions he needed, and I proposed using Coyoneda
(free Functor
) or even a free Applicative
to "pretend" to compute over Symbolic
values. When running an Ensure
hook, an actual value can be extracted from the state Concrete
using lowerCoyoneda
or retractAp
or whatever.
@ChickenProp then discovered that these existing free wrappers are not suitable for hedgehog
for two main reasons:
- They do not have the functor parameter as the final types argument, which means you have to manually write out instances for any
barbies
typeclasses, instead of using its generic-based deriving; and - These free wrappers lack a usable
Show
instance, which hedgehog requires.
He proposed a custom FVar
type, which I think has the following advantages and drawbacks:
- (pro) Has
FunctorB
andTraversableB
instances, and probably any others frombarbies
which we need; - (con?) Provides
instance (Eq a, Eq1 v) => Eq (FVar a v)
, butinstance Eq1 Symbolic
ignores its function argument, so this means that givenx :: Symbolic a
,FVar (const 0) x == FVar (const 1) x
will returnTrue
. This seems more like a "Symbolic
's fault", but I think we should not provide theEq
instance if we can get away without it; - (pro) Provides
instance Show (FVar a v)
, so it's actually usable withhedgehog
; - (con) Not a
Functor
, because the type arguments are the wrong way around; and - (con) Not possible to apply a function over multiple
Symbolic
values.
I think the ergonomic benefits justify building our own free wrapper. I also think we should consider going beyond a free Functor
to an analogue of a free Applicative
, either the one in free:Control.Applicative.Free.Fast
(which I don't really understand) or the one in free:Control.Applicative.Free.Final
. It seems like it'd be pretty useful to be able to lift pure functions across multiple Symbolic
values, and if #493 is accepted then we'll have instance Applicative Concrete
to make the retraction from a free Applicative
.
On the API side I have some suggestions. Here's what I think we should provide:
-- I think not working with `Var a v` will be more ergonomic, since if we get a good
-- way of returning multiple values from a `Command` in #459, we'll have various
-- `v a` values kicking around.
fvar :: Show a => v a -> FVar a v
fapply :: Show a => (a -> b) -> v a -> FVar b v
($$) = fapply -- infix alias
mapFVar :: (a -> b) -> FVar a v -> FVar b v
(<$$>) = mapFVar -- infix alias, same fixity as (<$>)
fconcrete :: FVar a Concrete -> a
fretract :: Functor/Applicative v => FVar a v -> v a -- Exact constraint depends on whether we build a free `Functor` or `Applicative`.
-- If you agree that we should go to a free `Appliactive`
apFVar :: FVar (a -> b) v -> FVar a v -> FVar b v
(<@@>) = apFVar -- infix alias, same fixity as (<*>). Note that (<**>) exists in `base` and so is unavailable. There's precedent for `<@>` being a weird variant of `<*>` in some FRP libraries.
liftFVar2 :: (a -> b -> c) -> FVar a v -> FVar b v -> FVar c v
liftFVar3 :: (a -> b -> c -> d) -> FVar a v -> FVar b v -> FVar c v -> FVar d v
(I don't have strong feelings about a free applicative here. I suppose the main benefit would be applying a function to two symbolic values and storing the result in state? I don't think I've had a use for this so far, but that's just me.)
On FVar
: I agree that the Eq
instance is weird. I think it would be reasonable to get rid of it, and keep Var
around for when people want Eq
and Ord
. (Or maybe it's more accurate to say I think the instance Eq1 Symbolic
is weird? I'm not sure. The law is liftEq (==) = (==)
and it does satisfy that. For a while I thought we might expect liftEq (\_ _ -> False) x x == False
, which the instance violates. But that doesn't hold for Maybe
or []
so it's clearly not a sensible thing to expect. So this instance does seem fine. And the Eq FVar
instance seems fine to me too. But somehow the combination seems weird.)
I suppose the main benefit would be applying a function to two symbolic values and storing the result in state?
Exactly. I can't imagine an exact use right now (I was sketching with a system of Users, Roles, and User<->Role attachments, but that seems doable with the current command type). Extending to a free Applicative
seems like it solves situations where you want to apply a pure function to 2..N Symbolic
values, and store that Symbolic
result somewhere that shouldn't see all the Symbolic
arguments (like the input of a Command
). These seem plausible and not hard to build support for (as opposed to, say, going all the way to a free Monad
), which is why I think it's worthwhile.
instance (...) => Eq (FVar a v)
is weird.
I think there's an additional coherence that we expect due to the relation between Concrete
and Symbolic
that makes this instance weird. (It's even worse for Ord
where the concrete values could compare very differently to the generated names.)
I vote that we scrap those Eq
/Ord
instances but keep instance Eq1/Ord1 Symbolic
and instance Eq/Ord (Symbolic a)
, which seem really important for maintaining e.g. sets of things generated from previous commands.