That's its intended use, actually. I findsendBundle too clumsy to work with in practice. Intended use is to newtypeBundle and then use rewrite/transform together with injBundle and runBundle to create actions/interpreters on the newtype.
newtypeAppioma=App(Bundle'[Input i, Output o]ma)runApp::Semri->(o->Semr())->Sem(Appio':r)a->SemrarunApponIonO=runOutputSemonO.runInputSem(raiseonI).runBundle.rewrite(\(Appbundle)->bundle)ping::forallior.Member(Appio)r=>o->Semr()pingo=transform(App@i@o.injBundle@(Outputo))(outputo)pong::forallior.Member(Appio)r=>Semripong=transform(App@i@o.injBundle@(Inputi))inputpingpong::forallior.Member(Appio)r=>o->Semripingpongo=dotransform(App@i@o.injBundle@(Outputo))(outputo)transform(App@i@o.injBundle@(Inputi))input
This also shows how you have the choice of between providing direct corresponding actions of the bundled effects (like ping and pong), or choosing only to expose certain combinations of these (like pingpong), similar to how you may restrictively expose power with normal newtypes.
All these ugly type applications arise as an effect of App being polymorphic (in i and o). You don't need them if your effect newtype is monomorphic, or if you use polysemy-plugin.
Effect newtypes aren't that special if you're only using them to represent first-order effects, since re/interpret and friends is a just as nice or nicer way to do the same job. They become more interesting if you want to build up a higher-order effect because, y'know, it allows you to avoid having to deal with interpretH.
dataFunctionabmcwhereApply::a->FunctionabmbmakeSem''FunctionrunPureFunction::(a->b)->InterpreterFor(Functionab)rrunPureFunctionf=interpret$\caseApplyx->pure$fxrunEffectfulFunction::(a->Semrb)->InterpreterFor(Functionab)rrunEffectfulFunctionf=interpret$\caseApplyx->fxrunCompose::forallabcrd.Member(Functionbc)(Functionab':Functionbc':r)=>(Sem(Functionbc':r)d->Semrd)->(Sem(Functionab':Functionbc':r)d->Sem(Functionbc':r)d)->Sem(Functionac':r)d->SemrdrunComposebcab=bc.ab.reinterpret2(\caseApplyx->apply@a@bx>>=apply@b@c)newtypeBoomerangabmc=Boomerang{unBoomerang::Bundle'[Function a b, Function b a]mc}runBoomerang::forallabrc.(Sem(Functionab':Functionba':r)c->Sem(Functionba':r)c)->(Sem(Functionba':r)c->Semrc)->Sem(Boomerangab':r)c->SemrcrunBoomerangabba=ba.ab.runBundle.rewriteunBoomerangforward::forallabr.Member(Boomerangab)r=>a->Semrbforwardx=transform(Boomerang@a@b.injBundle@(Functionab))(apply@a@bx)reverse::forallabr.Member(Boomerangab)r=>b->Semrareversex=transform(Boomerang@a@b.injBundle@(Functionba))(apply@b@ax)
Everything works up until reverse (although the type signatures of runCompose and runBoomerang are strange), but reverse gives me this compiler error: Could not deduce: a ~ b arising from a use of ‘injBundle’.
Ha! You just found one of the few cases where Member inference breaks even with type applications! Basically, Haskell can't tell that Function a b IS NOT Function b a, so the send is ambiguous (oversimplifying a bit; the reason forward works but reverse doesn't despite forward has the same ambiguity issue is due to how Member works.)
Solution is to import Polysemy.Membership and write this:
Interpreters that take interpreters as arguments often have this problem. If you're always gonna postcompose ab and bc, I recommend removing them as arguments to runCompose:
-- | An effect for calling functions.---- Examples:---- >>> (apply 1 >>= apply @Int @String >>= embed . putStrLn)-- & runPureFunction show-- & runPureFunction (+1)-- & runM-- 2---- >>> (apply 1 >>= embed . putStrLn)-- & subsumeCompose @Int @Int @String-- & runPureFunction show-- & runPureFunction (+1)-- & runM-- 2dataFunctionabmcwhereApply::a->FunctionabmbmakeSem''Function-- | Run this 'Function' as a pure function.runPureFunction::(a->b)->InterpreterFor(Functionab)rrunPureFunctionf=interpret$\caseApplyx->pure$fx-- | Run this 'Function' as a function with effects.runEffectfulFunction::(a->Semrb)->InterpreterFor(Functionab)rrunEffectfulFunctionf=interpret$\caseApplyx->fx-- | Run this 'Function' in terms of two other 'Function's which compose.-- The other function interpreters must come immediately after this.-- Note that any of the functions may or may not have side effects.runCompose::forallabcrd.Sem(Functionac':r)d->Sem(Functionab':Functionbc':r)drunCompose=reinterpret2$\caseApplyx->apply@a@bx>>=raise.apply@b@c-- | Run this 'Function' in terms of two other 'Function's which compose.-- The other function interpreters may be anywhere in the remaining effect stack.-- Note that any of the functions may or may not have side effects.subsumeCompose::forallabcrd.Members'[ Function a b , Function b c ]r=>InterpreterFor(Functionac)rsubsumeCompose=interpret$\caseApplyx->apply@a@bx>>=apply@b@c
And I have a Boomerang effect:
-- | An effect for wrapping pairs of invertible functions.newtypeBoomerangabmc=Boomerang{unBoomerang::Bundle'[Function a b, Function b a]mc}forward::Member(Boomerangab)r=>a->Semrbforward=transform(Boomerang.injBundle).applybackward::Member(Boomerangab)r=>b->Semrabackward=transform(Boomerang.Bundle(ThereHere)).apply-- | Run in terms of two 'Function's, either of which may have effects.-- The functions should be provided next.runBoomerang::Sem(Boomerangab':r)c->Sem(Functionab':Functionba':r)crunBoomerang=runBundle.rewriteunBoomerang-- | Run in terms of an isomorphism.runBoomerangIso::Iso'ab->Sem(Boomerangab':r)c->SemrcrunBoomerangIsol=runPureFunction(view$froml).runPureFunction(viewl).runBoomerang-- | Run in terms of two 'Function's, either of which may have effects.-- These functions can be anywhere in the effect stack, so can be reused for multiple 'Boomerang's.subsumeBoomerang::Members'[Function a b, Function b a]r=>InterpreterFor(Boomerangab)rsubsumeBoomerang=subsumeBundle.rewriteunBoomerang-- | Run this 'Boomerang' in terms of two other 'Boomerang's which compose (in both directions!)-- The other boomerangs must be run immediately after this.runComposeBoomerang::forallabcdr.Sem(Boomerangac':r)d->Sem(Boomerangab':Boomerangbc':r)drunComposeBoomerang=subsumeComposeBoomerang@a@b@c.raiseUnder2-- | Run this 'Boomerang' in terms of two other 'Boomerang's which compose (in both directions!)-- The other 'Boomerang's may appear anywhere in the effect stack.subsumeComposeBoomerang::forallabcr.Members'[Boomerang a b, Boomerang b c]r=>InterpreterFor(Boomerangac)rsubsumeComposeBoomerang=runEffectfulFunction(backward@b@c>=>backward@a@b).runEffectfulFunction(forward@a@b>=>forward@b@c).runBundle.rewriteunBoomerang
But that runComposeBoomerang doesn't compile. The error is similar to before, @Love Waern (King of the Homeless) , Couldn't match type ‘b’ with ‘a’ arising from a use of ‘subsumeComposeBoomerang’. But I can't see a way around it this time :/
I don't think there is one! subsumeComposeBoomerang has the problem I mentioned above: Haskell can't distinguish Function a b and Function b a, and thus when trying to resolve(Member (Boomerang a b) r, Member (Boomerang b a) r), it will attempt to unify a and b. Above, I fixed this by either raiseing or throwing in explicit membership proofs, but that trick only works when the effect stack r is at least partly known. In subsumeComposeBoomerang, ris completely polymorphic; you only have Member to work with, and in this case, it just doesn't work.
There are two ways to solve this. One way is to distinguish between the two effects by wrapping one in a newtype, and unwrapping them later. Tagged can be used for this purpose.
What I recommend instead is that you inline the definition ofsubsumeComposeBoomerangin runComposeBoomerang. That way, you can do the raise trick again.
Ah yes, that's why subsumeComposeBoomerang requires AllowAmbiguousTypes to compile. It's the same with subsumeCompose. The latter works though, once you give it concrete types to work with.
It distinguishes between e.g. Function Int Float and Function Float Int of course, but with polymorphic Function a b compiler can't be sure that a ~ b doesn't appear somewhere in the program
Can anyone give me an example of using
Bundle
? I can't even work out how to make a newtype using it :(https://funprog.zulipchat.com/#narrow/stream/216942-Polysemy/topic/wrap.20higher-order.20effect/near/190317850
I did some live coding on here figuring it out :laughing:
Ah, so it isn't used with newtype at all? I was imagining something along the lines of
well I guess you can do that, but it's not necessary
That's its intended use, actually. I find
sendBundle
too clumsy to work with in practice. Intended use is tonewtype
Bundle
and then userewrite
/transform
together withinjBundle
andrunBundle
to create actions/interpreters on the newtype.Here's an example, by using your
App
:This also shows how you have the choice of between providing direct corresponding actions of the bundled effects (like
ping
andpong
), or choosing only to expose certain combinations of these (likepingpong
), similar to how you may restrictively expose power with normal newtypes.All these ugly type applications arise as an effect of
App
being polymorphic (ini
ando
). You don't need them if your effect newtype is monomorphic, or if you usepolysemy-plugin
.Effect newtypes aren't that special if you're only using them to represent first-order effects, since
re/interpret
and friends is a just as nice or nicer way to do the same job. They become more interesting if you want to build up a higher-order effect because, y'know, it allows you to avoid having to deal withinterpretH
.A very neat example I just thought of is a
Mask
effect that relies onFinal IO
:Although this doesn't need
Bundle
.awesome
Thanks @Love Waern (King of the Homeless), that example is very helpful!
I'm having trouble getting this thing to work:
Everything works up until
reverse
(although the type signatures ofrunCompose
andrunBoomerang
are strange), butreverse
gives me this compiler error:Could not deduce: a ~ b arising from a use of ‘injBundle’
.(eventually I want to be able to compose Boomerangs, so that I can build a decode/encode chain out of a mixture of pure and effectful functions)
I think I need a function of type
(Sem (e1 ': r) a -> Sem r a) -> Sem (e1 ': e2 ': r) a -> Sem (e2 ': r) a
Ha! You just found one of the few cases where
Member
inference breaks even with type applications! Basically, Haskell can't tell thatFunction a b
IS NOTFunction b a
, so the send is ambiguous (oversimplifying a bit; the reasonforward
works butreverse
doesn't despiteforward
has the same ambiguity issue is due to howMember
works.)Solution is to import
Polysemy.Membership
and write this:That should fix it.
I think. Haven't checked.
Yes, that fixes it. Thanks :)
What about the strange types of
runCompose
andrunBoomerang
? Is there a way to simplify these?For
runBoomerang
, unfortunately not really. ForrunCompose
, only a tiny bit:Interpreters that take interpreters as arguments often have this problem. If you're always gonna postcompose
ab
andbc
, I recommend removing them as arguments torunCompose
:And then apply
ab
andbc
through simple composition.And of course, the same thing can be done for
runBoomerang
:Ok, I'll work with that :)
Ok, I'm nearly there. I have a
Function
effect:And I have a
Boomerang
effect:But that
runComposeBoomerang
doesn't compile. The error is similar to before, @Love Waern (King of the Homeless) ,Couldn't match type ‘b’ with ‘a’ arising from a use of ‘subsumeComposeBoomerang’
. But I can't see a way around it this time :/I don't think there is one!
subsumeComposeBoomerang
has the problem I mentioned above: Haskell can't distinguishFunction a b
andFunction b a
, and thus when trying to resolve(Member (Boomerang a b) r, Member (Boomerang b a) r)
, it will attempt to unifya
andb
. Above, I fixed this by eitherraise
ing or throwing in explicit membership proofs, but that trick only works when the effect stackr
is at least partly known. InsubsumeComposeBoomerang
,r
is completely polymorphic; you only haveMember
to work with, and in this case, it just doesn't work.There are two ways to solve this. One way is to distinguish between the two effects by wrapping one in a newtype, and unwrapping them later.
Tagged
can be used for this purpose.What I recommend instead is that you inline the definition of
subsumeComposeBoomerang
inrunComposeBoomerang
. That way, you can do theraise
trick again.See if that works.
Ah yes, that's why
subsumeComposeBoomerang
requiresAllowAmbiguousTypes
to compile. It's the same withsubsumeCompose
. The latter works though, once you give it concrete types to work with.Yes, your
runComposeBoomerang
works :)It took me a while to work out how the
raise
trick works, but I think I have it now.I don't see why Haskell can't distinguish between
Function a b
andFunction b a
though, at least when usingScopedTypeVariables
andTypeApplications
.It distinguishes between e.g.
Function Int Float
andFunction Float Int
of course, but with polymorphicFunction a b
compiler can't be sure thata ~ b
doesn't appear somewhere in the programOh, I think I see, because it can't exclude that possibility, it can't exclude using the first effect on the stack.