Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a free Functor or Applicative wrapper to enable pretend computations over Symbolic values #494

Open
endgame opened this issue Jun 20, 2023 · 2 comments

Comments

@endgame
Copy link
Contributor

endgame commented Jun 20, 2023

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:

  1. 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
  2. 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 and TraversableB instances, and probably any others from barbies which we need;
  • (con?) Provides instance (Eq a, Eq1 v) => Eq (FVar a v), but instance Eq1 Symbolic ignores its function argument, so this means that given x :: Symbolic a, FVar (const 0) x == FVar (const 1) x will return True. This seems more like a "Symbolic's fault", but I think we should not provide the Eq instance if we can get away without it;
  • (pro) Provides instance Show (FVar a v), so it's actually usable with hedgehog;
  • (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
@ChickenProp
Copy link
Contributor

(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.)

@endgame
Copy link
Contributor Author

endgame commented Jun 22, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants