Bundle example - Polysemy

Welcome to the Functional Programming Zulip Chat Archive. You can join the chat here.

Alex Chapman

Can anyone give me an example of using Bundle? I can't even work out how to make a newtype using it :(

Alex Chapman

Ah, so it isn't used with newtype at all? I was imagining something along the lines of

newtype App i o m a = App (Bundle ('[Input i, Output o] m a))
Torsten Schmits

well I guess you can do that, but it's not necessary

Love Waern (King of the Homeless)

That's its intended use, actually. I findsendBundle too clumsy to work with in practice. Intended use is to newtype Bundle and then use rewrite/transform together with injBundle and runBundle to create actions/interpreters on the newtype.

Love Waern (King of the Homeless)

Here's an example, by using your App:

newtype App i o m a = App (Bundle '[Input i, Output o] m a)

runApp :: Sem r i -> (o -> Sem r ()) -> Sem (App i o ': r) a -> Sem r a
runApp onI onO =
    runOutputSem onO
  . runInputSem (raise onI)
  . runBundle
  . rewrite (\(App bundle) -> bundle)

ping :: forall i o r. Member (App i o) r => o -> Sem r ()
ping o = transform (App @i @o . injBundle @(Output o)) (output o)

pong :: forall i o r. Member (App i o) r => Sem r i
pong = transform (App @i @o . injBundle @(Input i)) input

pingpong :: forall i o r. Member (App i o) r => o -> Sem r i
pingpong o = do
  transform (App @i @o . injBundle @(Output o)) (output o)
  transform (App @i @o . injBundle @(Input i)) 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.

Love Waern (King of the Homeless)

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.

Love Waern (King of the Homeless)

A very neat example I just thought of is a Mask effect that relies on Final IO:

import qualified Control.Exception as X
import Polysemy.Final

newtype Mask m a = Mask (Final IO m a)

mask :: Member Mask r => ((forall x. Sem r x -> Sem r x) -> Sem r a) -> Sem r a
mask main = transform Mask $ withWeavingToFinal $ \s wv _ -> X.mask $ \restore -> do
  let
    restore' :: Member Mask r => Sem r x -> Sem r x
    restore' m = transform Mask $ withWeavingToFinal $ \s' wv' _ -> restore $ wv' (raise m <$ s')
  wv (raise (main restore') <$ s)

maskToIO :: Member (Final IO) r => Sem (Mask ': r) a -> Sem r a
maskToIO = transform (\(Mask e) -> e)

Although this doesn't need Bundle.

Alex Chapman

Thanks @Love Waern (King of the Homeless), that example is very helpful!

Alex Chapman

I'm having trouble getting this thing to work:

data Function a b m c where
  Apply :: a -> Function a b m b

makeSem ''Function

runPureFunction :: (a -> b) -> InterpreterFor (Function a b) r
runPureFunction f = interpret $ \case
  Apply x -> pure $ f x

runEffectfulFunction :: (a -> Sem r b) -> InterpreterFor (Function a b) r
runEffectfulFunction f = interpret $ \case
  Apply x -> f x

runCompose
  :: forall a b c r d.
  Member (Function b c) (Function a b ': Function b c ': r)
  => (Sem (Function b c ': r) d -> Sem r d)
  -> (Sem (Function a b ': Function b c ': r) d -> Sem (Function b c ': r) d)
  -> Sem (Function a c ': r) d -> Sem r d
runCompose bc ab = bc . ab . reinterpret2 (\case
  Apply x -> apply @a @b x >>= apply @b @c)

newtype Boomerang a b m c = Boomerang { unBoomerang :: Bundle '[Function a b, Function b a] m c }

runBoomerang
  :: forall a b r c.
  (Sem (Function a b ': Function b a ': r) c -> Sem (Function b a ': r) c)
  -> (Sem (Function b a ': r) c -> Sem r c)
  -> Sem (Boomerang a b ': r) c -> Sem r c
runBoomerang ab ba = ba . ab . runBundle . rewrite unBoomerang

forward :: forall a b r. Member (Boomerang a b) r => a -> Sem r b
forward x = transform (Boomerang @a @b . injBundle @(Function a b)) (apply @a @b x)

reverse :: forall a b r. Member (Boomerang a b) r => b -> Sem r a
reverse x = transform (Boomerang @a @b . injBundle @(Function b a)) (apply @b @a x)

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’.

Alex Chapman

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

Alex Chapman

I think I need a function of type (Sem (e1 ': r) a -> Sem r a) -> Sem (e1 ': e2 ': r) a -> Sem (e2 ': r) a

Love Waern (King of the Homeless)

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:

reverse x = transform (Boomerang @a @b . Bundle (There Here)) (apply @b @a)

That should fix it.

Alex Chapman

Yes, that fixes it. Thanks :)

Alex Chapman

What about the strange types of runCompose and runBoomerang? Is there a way to simplify these?

Love Waern (King of the Homeless)

For runBoomerang, unfortunately not really. For runCompose, only a tiny bit:

runCompose
  :: forall a b c r d
   . (Sem (Function b c ': r) d -> Sem r d)
  -> (Sem (Function a b ': Function b c ': r) d -> Sem (Function b c ': r) d)
  -> Sem (Function a c ': r) d -> Sem r d
runCompose bc ab = bc . ab . reinterpret2 (\case
  Apply x -> apply @a @b x >>= raise . apply @b @c)

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:

runCompose
  :: forall a b c r d
   . Sem (Function a c ': r) d -> Sem (Function a b ': Function b c ': r) d
runCompose = reinterpret2 $ \case
  Apply x -> apply @a @b x >>= raise . apply @b @c

And then apply ab and bc through simple composition.

Love Waern (King of the Homeless)

And of course, the same thing can be done for runBoomerang:

runBoomerang
  :: forall a b r c.
    Sem (Boomerang a b ': r) c -> Sem (Function a b ': Function b a ': r) c
runBoomerang = runBundle . rewrite unBoomerang
Alex Chapman

Ok, I'll work with that :)

Alex Chapman

Ok, I'm nearly there. I have a Function effect:

-- | 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
-- 2
data Function a b m c where
  Apply :: a -> Function a b m b

makeSem ''Function

-- | Run this 'Function' as a pure function.
runPureFunction :: (a -> b) -> InterpreterFor (Function a b) r
runPureFunction f = interpret $ \case
  Apply x -> pure $ f x

-- | Run this 'Function' as a function with effects.
runEffectfulFunction :: (a -> Sem r b) -> InterpreterFor (Function a b) r
runEffectfulFunction f = interpret $ \case
  Apply x -> f x

-- | 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
  :: forall a b c r d
   . Sem (Function a c ': r) d -> Sem (Function a b ': Function b c ': r) d
runCompose = reinterpret2 $ \case
  Apply x -> apply @a @b x >>= 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
  :: forall a b c r d
   . Members
     '[ Function a b
      , Function b c
      ] r
  => InterpreterFor (Function a c) r
subsumeCompose = interpret $ \case
  Apply x -> apply @a @b x >>= apply @b @c

And I have a Boomerang effect:

-- | An effect for wrapping pairs of invertible functions.
newtype Boomerang a b m c = Boomerang
  { unBoomerang :: Bundle '[Function a b, Function b a] m c }

forward :: Member (Boomerang a b) r => a -> Sem r b
forward = transform (Boomerang . injBundle) . apply

backward :: Member (Boomerang a b) r => b -> Sem r a
backward = transform (Boomerang . Bundle (There Here)) . apply

-- | Run in terms of two 'Function's, either of which may have effects.
-- The functions should be provided next.
runBoomerang
  :: Sem (Boomerang a b ': r) c -> Sem (Function a b ': Function b a ': r) c
runBoomerang = runBundle . rewrite unBoomerang

-- | Run in terms of an isomorphism.
runBoomerangIso
  :: Iso' a b -> Sem (Boomerang a b ': r) c -> Sem r c
runBoomerangIso l =
  runPureFunction (view $ from l)
  . runPureFunction (view l)
  . 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 (Boomerang a b) r
subsumeBoomerang = subsumeBundle . rewrite unBoomerang

-- | 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
  :: forall a b c d r
   . Sem (Boomerang a c ': r) d
   -> Sem (Boomerang a b ': Boomerang b c ': r) d
runComposeBoomerang = 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
  :: forall a b c r
   . Members '[Boomerang a b, Boomerang b c] r
  => InterpreterFor (Boomerang a c) r
subsumeComposeBoomerang =
  runEffectfulFunction (backward @b @c >=> backward @a @b)
  . runEffectfulFunction (forward @a @b >=> forward @b @c)
  . runBundle
  . rewrite unBoomerang

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 :/

Love Waern (King of the Homeless)

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.

runComposeBoomerang
  :: forall a b c d r
   . Sem (Boomerang a c ': r) d
   -> Sem (Boomerang a b ': Boomerang b c ': r) d
runComposeBoomerang =
    runEffectfulFunction (raise . backward @b @c >=> backward @a @b)
  . runEffectfulFunction (forward @a @b >=> raise . raise . forward @b @c)
  . runBundle
  . rewrite unBoomerang
  . raiseUnder2

See if that works.

Alex Chapman

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.

Alex Chapman

Yes, your runComposeBoomerang works :)

Alex Chapman

It took me a while to work out how the raise trick works, but I think I have it now.

Alex Chapman

I don't see why Haskell can't distinguish between Function a b and Function b a though, at least when using ScopedTypeVariables and TypeApplications.

TheMatten

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

Alex Chapman

Oh, I think I see, because it can't exclude that possibility, it can't exclude using the first effect on the stack.