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

Membership proof rewrite, membership testing, compose effect #282

Merged

Conversation

KingoftheHomeless
Copy link
Collaborator

Making this a draft since the Compose effect is WIP.

Closes #281 if we're happy with KnownRow+subsumeUsing.

. KnownList r'
=> Union r m a
-> Union (Append r' r) m a
weakenList u = unconsKnownList @_ @r' u (\_ (_ :: Proxy r'') -> weaken (weakenList @r'' u))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are subsumeMembership, extendMembership, and weakenList generally interesting enough to warrant a spot in an internal module not named Polysemy.Compose.Internal?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dream of a day when we can use the same subsume and raise and weaken etc. functions, regardless of whether we're working on a Union, Membership or Sem.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to put these in Polysemy.Bundle.Internal for now.

@KingoftheHomeless KingoftheHomeless changed the title Proof membership rewrite, membership testing, compose effect Membership proof rewrite, membership testing, compose effect Nov 24, 2019
@isovector
Copy link
Member

I quite like this from a casual look through --- it's very elegant and much easier to follow than my original implementation. Thanks for putting it together; I'll go through more closely later today!

-> Union (Append r' r) m a
weakenList u = unconsKnownList @_ @r' u (\_ (_ :: Proxy r'') -> weaken (weakenList @r'' u))

class KnownList (l :: [k]) where
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have some source/writeup/blogpost for this - I haven't encountered it before it and it seems rather interesting?(especially the constraints in unconsKnownList)

Copy link
Collaborator Author

@KingoftheHomeless KingoftheHomeless Nov 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I adapted a trick I learned ways back (don't remember where from) for pattern-matching on type-level expressions at run-time.
I don't actually know of a writeup for this, but the original pattern came from the following way of encoding type-level naturals:

data Nat = Z | S Nat

class KnownNat (n :: Nat) where
  induceNat :: (n ~ 'Z => a) -> (forall n'. (n ~ ('S n'), KnownNat n') => Proxy n' -> a) -> a

instance KnownNat 'Z where
  induceNat b _ = b

instance KnownNat n => KnownNat ('S n) where
  induceNat _ i = i Proxy

Basically, given a type-level Nat, you can do induction on it by providing what you'll do in case it's 'Z -- and when defining that case, you get to know that n ~ 'Z -- and what you'll do in case it's 'S n' -- and in that case, you get to know that n ~ 'S n'. This is the same pattern, except for lists instead.

I think nowadays this pattern has been replaced by singletons, although I'm not sure.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to be this symmetry between existential wrapper/CPS - singleton basically encodes those possible cases as datatype instead of call to continuation.

@KingoftheHomeless
Copy link
Collaborator Author

We probably want a module to expose ElemOf(..), membership, subsumeUsing, KnownRow, tryMembership, and exposeUsing, right? What should it be called? Polysemy.Membership?

@isovector
Copy link
Member

Can you walk me through what the Compose effect does?

@KingoftheHomeless
Copy link
Collaborator Author

It's just an effect for collecting a number of effects into one umbrella, mainly so that you can make effect newtypes more powerful.

For example:

newtype Bridge i o m a = Bridge { unBridge :: Compose '[Input i, Output o] m a }

injCompose :: Member e r => e m a -> Compose r m a
injCompose = Compose membership

receive :: Member (Bridge i o) r => Sem r i
receive = transform (Bridge . injCompose) input

deliver :: Member (Bridge i o) r => o -> Sem r ()
deliver o = transform (Bridge . injCompose) (output o) 

runBridge :: Sem r i -> (o -> Sem r ()) -> Sem (Bridge i o ': r) a -> Sem r a
runBridge inp out =
    runOutputSem out
  . runInputSem inp
  . runCompose
  . rewrite unBridge

Haven't checked if this type-checks, but you get the gist. I should actually offer injCompose, didn't think about rewrite/transform.

@isovector
Copy link
Member

Thanks, that's super helpful and cool!

src/Polysemy/Compose.hs Outdated Show resolved Hide resolved
@@ -351,8 +356,8 @@ raiseUnder :: ∀ e2 e1 r a. Sem (e1 ': r) a -> Sem (e1 ': e2 ': r) a
raiseUnder = hoistSem $ hoist raiseUnder . weakenUnder
where
weakenUnder :: ∀ m x. Union (e1 ': r) m x -> Union (e1 ': e2 ': r) m x
weakenUnder (Union SZ a) = Union SZ a
weakenUnder (Union (SS n) a) = Union (SS (SS n)) a
weakenUnder (Union Here a) = Union Here a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a way to generalize this with the new machinery? I've always hated having to write a few blessed raise functions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, there's nothing that the new membership proof machinery can help on this point.

This should be implementable, though:

type family Underneath n r e where
  Underneath 'Z r e = e ': r
  Underneath ('S n) (x ': r) e = x ': Underneath n r e

raiseUnderX :: SNat n -> Sem r a -> Sem (Underneath n r e) a

------------------------------------------------------------------------------
-- | A class for effect rows whose elements are inspectable.
--
-- This constraint is eventually satisfied as @r@ is instantied to a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment suggests that this thing can never get stuck. Is that true? If so, how does it work?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is just so newcomers won't get scared about "what do I need to have KnownRow be satisfied?"; the instances defined take care of it eventually.

It won't always work out so cleanly. Given a type variable s, in order for State s ': r to be KnownRow you need to specify that s is a Typeable.

------------------------------------------------------------------------------
-- | Given @'Member' e r@, extract a proof that @e@ is an element of @r@.
membership :: Member e r => ElemOf r e
membership = membership'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the indirection?

Copy link
Collaborator Author

@KingoftheHomeless KingoftheHomeless Nov 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly to get right order on type variables, but mainly so that we have Member as the constraint, rather than the internal Find.

@isovector
Copy link
Member

I really like this. Left lots of comments because I don't 100% understand it yet, but the reification feels right.

@isovector
Copy link
Member

I think I've lost the forest for the trees here. Can you give a paragraph summary of what this accomplishes (even if it's just making the Union module more elegant)

@KingoftheHomeless
Copy link
Collaborator Author

So the old proof had two components to it -- SNat n and IndexOf r n ~ e. More than simply making things ugly, the fact that part of the proof is a type equality constraint on IndexOf makes it hard to work with. Notably, pattern matching on the SNat n tells the type system nothing about r! You can't deduce r ~ x ': r' from n ~ 'Z or n ~ S n'. IIRC, I couldn't implement runCompose under the old proof mechanism because of this.

When it comes to reification, a recent thought I had makes the gains there less concrete. A Member constraint dictionary à-la Has in #281 has the flaws I detailed in #281 (comment). However, a proper reification is possible through the following:

data Membership r e where
  Membership :: (IndexOf r n ~ e) => SNat n -> Membership r e

This probably works; you should be able to base KnownRow, subsumeUsing, exposeUsing on it. Still, it's pretty ugly (and inefficient) that you need a wrapper, especially considering it's only needed because the proof is partly represented through a constraint when it doesn't need to be.

In short: this solution is prettier, as the entire proof is now represented by one datatype, and makes playing around with membership more cleaner. In addition, ElemOf provides extra information that makes certain stuff that was impossible before now possible, and it shouldn't affect performance.

@isovector
Copy link
Member

isovector commented Nov 28, 2019 via email

@KingoftheHomeless KingoftheHomeless marked this pull request as ready for review December 2, 2019 08:13
@KingoftheHomeless KingoftheHomeless merged commit a4868bd into polysemy-research:master Dec 8, 2019
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

Successfully merging this pull request may close these issues.

Conditional effects
4 participants