-
Notifications
You must be signed in to change notification settings - Fork 107
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
Return multiple Vars from a single Command #459
Comments
This is an interesting idea. My usually approach for returning multiple values is to work within the framework that Hedgehog gives us, but to then essentially store defunctionalized functions to extract elements where I need them. So let's say you have something that outputs data State f = State
{ foos :: [Expr FooId f]
, bars :: [Expr BarId f]
} And I then create a command type like: data CallBar f = CallBar
{ fooId :: Expr FooId } The trick here is the data Expr a f where
V :: Var a f -> Expr a f
Fst :: Expr (a, b) f -> Expr a f
Snd :: Expr (a, b) f -> Expr b f You would also write an evaluator for this, when you have eval :: Expr a Concrete -> a
eval = \case
V v -> concrete v
Fst e -> fst $ eval e
Snd e -> snd $ eval e So now when my first command completes, I can provide a Update $ \s input outputV -> s
{ foos = Fst (V outputV) : foos s
, bars = Snd (V outputV) : bars s
} I haven't done a huge amount with this, but I thought it was worth sharing an alternative approach. |
Oh, I now think these constraints are too weak if we want to avoid runtime crashes. The problem is that we need to be able to pair off the vars created during the symbolic run with those created during the concrete run. That's fine with something like But it doesn't work with something like newtype ListB a f = ListB [f a] which I think (haven't fully checked) can have all the relevant instances. One could define bpure _ = ListB []
bpure x = ListB [x]
bpure x = ListB $ replicate n x -- for any `n`
bpure x = ListB (repeat x) and the execute function can return any number of Concretes. From the class documentation, it sounds like DistributiveB gives the constraints we want? But I haven't taken the time to understand it properly. I don't even have a good handle on regular Distributive. I'm also less confident than I used to be that it would work for things like So probably this needs more exploration, which I'm unlikely to get to in the immediate future. In the meantime @ocharles' suggestion is a neat trick. |
@ocharles that's a huge help, thanks! |
The documentation for
This is probably why your I think data Symbolic a where
Symbolic :: TypeRep b -> Name -> (b -> a) -> Symbolic a
instance Functor Symbolic where
fmap f (Symbolic rep n g) = Symbolic rep n $ f . g Then you can do something like this: data Record f = Record
{ foo :: f Int,
bar :: f Char
}
deriving stock (Generic)
deriving anyclass (FunctorB, DistributiveB)
deriving instance (forall a. Show (f a)) => Show (Record f)
-- Assume this is passed into an Update hook; we wouldn't really make one by hand
rSym :: Symbolic (Record Identity)
rSym = symbol (Name 0)
rSymParts :: Record Symbolic
rSymParts = bdistribute' rSym And then I think you're basically there, modulo writing some slightly-gnarly library code. I think you have to make the |
I just noticed how similar this is to what @ocharles is doing, but it's able to hold arbitrary functions next to a data Record v = Record
{ foo :: Coyoneda v Int,
bar :: Coyoneda v Char,
baz :: String
}
-- Imagine this is in an 'Update' callback.
-- When you are in an 'Ensure' callback, you'll have `Coyoneda Concrete a` everywhere;
-- `Concrete` is a functor so you can `lowerCoyoneda` to get your specific `a`.
update :: Ord1 v => Record v -> Var (Int, Char) v -> Record v
update r (Var v) = r
{ foo = fst <$> liftCoyoneda v,
bar = snd <$> liftCoyoneda v
} But I think passing around an |
@endgame thanks for looking into this! Coyoneda seems neat. Note that your proposed definition of But I'm not sure we need the
|
Ah yes. It's probably important to preserve Why am I interested in something with a bdistribute :: (DistributiveB b, Functor f) => f (b g) -> b (Compose f g)
bdistribute' :: (DistributiveB b, Functor f) => b f -> f (b Identity) Any HKD record type where every field is wrapped by the I think that if we want to change There are two ways to do this that I can see:
I think having the |
(Aside: It would also be nice to have |
I think I agree with this, though in my case it's mostly for the specific reason of "I want to return multiple vars at once" rather than consistency. (I'm trying out the
So the reason I thought newtype ListB a f = ListB [f a] then you might end up with But I'm actually not entirely sure |
Sure. I take consistency and symmetry as a smell that the design is pointing the right way, and the current way
I believe this is just a limitation in the generics-based deriving code, but nothing fundamental: -- Dunno if this is the best name
traverseCoyoneda :: Functor e => (forall x. f x -> e (g x)) -> Coyoneda f a -> e (Coyoneda g a)
traverseCoyoneda f (Coyoneda g fb) = Coyoneda g <$> f fb
data X f = X
{ x1 :: f Int,
x2 :: Coyoneda f Char,
x3 :: Bool
}
instance FunctorB X where
bmap :: (forall a. f a -> g a) -> X f -> X g
bmap f x = X {x1 = f $ x1 x, x2 = hoistCoyoneda f $ x2 x, x3 = x3 x}
instance TraversableB X where
btraverse :: Applicative e => (forall a. f a -> e (g a)) -> X f -> e (X g)
btraverse f x = do
x1' <- f $ x1 x
x2' <- traverseCoyoneda f $ x2 x
pure X {x1 = x1', x2 = x2', x3 = x3 x} Re: Re: Can |
Looks like it wasn't just about automatic deriving, there are also issues with -- | This allows us to deconstruct the return value from an @exec@ function and
-- store it in state.
--
-- If an @exec@ function returns @(a, b)@, we might want to put the @a@ and @b@
-- in state separately. But we also need to put them in `Symbolic` state, where
-- what we have is @val :: `Symbolic` (a, b)@. Since `Symbolic` isn't a functor,
-- we can't really do that. What we can do is store @(fst, val)@ and
-- @(snd, val)@, and this is essentially that.
--
-- This is a flipped version of @Coyoneda@ from
-- https://hackage.haskell.org/package/kan-extensions-5.2.5/docs/Data-Functor-Coyoneda.html.
-- Flipping the type variables makes it suitable for things using it to
-- auto-derive `FunctorB` and `TraversableB`. We also add a @Show b@ constraint,
-- because the `Show` instance for @Coyoneda f a@ requires @Functor f@, which
-- we still don't have for `Symbolic`.
data FVar a v where
FVar :: Show b => (b -> a) -> v b -> FVar a v
instance (Eq1 v, Eq a) => Eq (FVar a v) where
(FVar f1 a1) == (FVar f2 a2) = liftEq (\x y -> f1 x == f2 y) a1 a2
instance (Show1 v, Show a) => Show (FVar a v) where
showsPrec prec (FVar _ x) =
showParen (prec > 10) $ showString "FVar _ " . showsPrec1 11 x
instance FunctorB (FVar a) where
bmap f (FVar g x) = FVar g (f x)
instance TraversableB (FVar a) where
btraverse f (FVar g x) = FVar g <$> f x
-- | Turn a `Var` into an `FVar`.
fvar :: Show b => (b -> a) -> Var b v -> FVar a v
fvar f (Var x) = FVar f x
-- | Pull a `Concrete` value out of an `FVar`.
--
-- There's a more general but less convenient form of this, which has type
-- @`Functor` v => `FVar` a v -> v a@.
fconcrete :: FVar a Concrete -> a
fconcrete (FVar f (Concrete x)) = f x I still think that actually returning multiple Vars would be nicer, in particular it would improve the show instance. (Right now we'll see something like
I think we do need to pair them off. Right now we have function insertConcrete :: Symbolic a -> Concrete a -> Environment -> Environment and we need some analog of that with multiple vars. |
Good digging. I made a new issue to discuss free wrappers, so we can focus on the "multiple return values" problem here. Actually generating an brecompose' :: FunctorB b => b (Compose ((->) a) f) -> a -> b f
brecompose' b a = bmap (($ a) . getCompose) b Perhaps it is a red herring? Generating a Trade-offs, trade-offs. Which ones to pick? |
I have a state machine where one Command's output type is
(FooId, BarId)
. That gets put into the model, so I haveids :: Var (FooId, BarId) v
in my state. Then any time I want to pass eitherFooId
orBarId
into acommandExecute
, it needs to get both of them and deconstruct.I think it would be possible to change things so each Command can return multiple Vars at once. The state would get
fooId :: Var FooId v, barId :: Var BarId v
, the same as if they'd come from different Commands.The sketch is that
output
would move from being kindType
to(Type -> Type) -> Type
likeinput
, parameterized bySymbolic
orConcrete
.commandExecute
returnsm (output Concrete)
.Update
getsoutput v
instead ofVar output v
, andEnsure
getsoutput Concrete
instead of justoutput
. Where I currently return(fooId, barId)
, I'd instead returnTuple2B (Concrete fooId, Concrete barId)
, whereThen we need some instances from barbies to generate the vars. From a bit of exploration, I think giving
Tuple2B a b
instances ofApplicativeB
,TraversableB
andConstraintsB
is enough.bpure
lets us generateTuple2B (Const ()) (Const ()) :: Tuple2B a b (Const ())
, and then we canbtraverseC
to generate aName
for each of thoseConst
s. (We needConstraintsB
because of theTypeable
constraint inSymbolic
.)(I think that would need us to require barbies >= 2.0.0.0, but my guess is that wouldn't cause problems? It only has two dependencies itself, base >= 4.11 and transformers, and it was released in Jan 2020.)
This does make single-var Commands a bit more complicated, they now return
Var (Concrete x)
instead of justx
. (Or I think we'd want a newnewtype SoloB a f = SoloB (f a)
on the grounds thatVar
feels like it only works here "by coincidence". I'm a bit surprised there's nothing likeSoloB
in the barbies package.) Commands currently returning()
can just return barbies'Unit
. I think it would be possible to do this mostly backwards-compatible, similar to how #456 does it, though perhaps more awkward.I've made a bit of progress on this that makes me think it works, but not yet in a sharable state. If it's wanted I can finish it off.
The text was updated successfully, but these errors were encountered: