-
Notifications
You must be signed in to change notification settings - Fork 52
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
Derivable Algebra #359
Derivable Algebra #359
Conversation
class (HFunctor sig, Monad m) => Algebra sig m | m -> sig where | ||
class Monad m => Algebra sig m | m -> sig where | ||
-- | Construct a value in the carrier for an effect signature (typically a sum of a handled effect and any remaining effects). | ||
alg :: sig m a -> m a | ||
alg :: Monad n => (forall x . n x -> m x) -> sig n a -> m a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the key change: sig
now holds n
s instead of m
s, and a monad homomorphism is provided to map them into the carrier.
send = alg . inj | ||
send = alg id . inj |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
send
always operates in the topmost algebra, so we start by just passing id
.
alg (Choose m) = m True S.<> m False | ||
alg hom (Choose m) = hom (m True) S.<> hom (m False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Terminal algebras only need to apply the homomorphism to any continuations or higher-order positions to produce an action in their carrier.
alg (L (L (Throw e))) = Except.throwE e | ||
alg (L (R (Catch m h k))) = Except.catchE m h >>= k | ||
alg (R other) = Except.ExceptT $ alg (thread (Right ()) (either (pure . Left) Except.runExceptT) other) | ||
alg hom = \case | ||
L (L (Throw e)) -> Except.throwE e | ||
L (R (Catch m h k)) -> Except.catchE (hom m) (hom . h) >>= hom . k | ||
R other -> Except.ExceptT $ alg id (thread (Right ()) (either (pure . Left) (Except.runExceptT . hom)) other) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nonterminal algebras, on the other hand, also need to apply the homomorphism before threading through the rest of the signature.
alg (L (Ask k)) = Reader.ask >>= k | ||
alg (L (Local f m k)) = Reader.local f m >>= k | ||
alg (R other) = Reader.ReaderT $ \ r -> alg (hmap (`Reader.runReaderT` r) other) | ||
alg hom = \case | ||
L (Ask k) -> Reader.ask >>= hom . k | ||
L (Local f m k) -> Reader.local f (hom m) >>= hom . k | ||
R other -> Reader.ReaderT $ \ r -> alg ((`Reader.runReaderT` r) . hom) other |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Algebras using hmap
or handleCoercible
can instead compose their handler onto the homomorphism passed to alg
for the rest of the stack.
dst :: ChooseC Identity (n a) -> m (ChooseC Identity a) | ||
dst = runIdentity . runChoose (liftA2 (liftA2 (<|>))) (pure . runChoose (liftA2 (<|>)) (pure . pure) . hom) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We use ScopedTypeVariables
now since we close over hom
. We could probably have removed the signature for dst
instead, but the signatures for these are a bit subtle since they use the same carrier type but containing Identity
, so I wanted to leave the signatures intact.
newtype CullC m a = CullC (ReaderC Bool (NonDetC m) a) | ||
newtype CullC m a = CullC { runCullC :: ReaderC Bool (NonDetC m) a } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I introduced selectors for a few types like this, but I don’t export them. I could maybe have used coerce . hom
in the algebra instead of runCullC . hom
, but it should be eliminated either way, and this way is clearer.
deriving (Applicative, Functor, Monad, Fail.MonadFail, MonadFix, MonadIO, MonadTrans) | ||
deriving (Algebra (Error e :+: sig), Applicative, Functor, Monad, Fail.MonadFail, MonadFix, MonadIO, MonadTrans) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is one of the only cases where we define a carrier wrapping another one just so that we can customize some instance other than Algebra
, and so is likewise one of a small number of cases where we can derive an Algebra
instance.
{ runHandler :: forall s x . sig (InterpretC s sig m) x -> InterpretC s sig m x } | ||
{ runHandler :: forall s n x . Monad n => (forall y . n y -> InterpretC s sig m y) -> sig n x -> InterpretC s sig m x } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has to take a homomorphism just like alg
, or else we have to keep HFunctor
around. I think this is reasonable: the whole idea of runInterpret
is that it allows you to write “a real algebra” inline.
handleCoercible :: (HFunctor sig, Functor f, Coercible f g) => sig f a -> sig g a | ||
handleCoercible = hmap coerce |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All uses of hmap
in the library are subsumed by the homomorphism parameter to alg
, so we can eliminate handleCoercible
along with the rest.
This PR changes
Algebra
’salg
method, giving it a parameter for a monad homomorphism which is applied to actions within the signature.Some neat effects of this:
Algebra
instances usingGeneralizedNewtypeDeriving
orDerivingVia
.hmap
in the case of e.g.ReaderC
.handleCoercible
in the case of non-orthogonal carriers.HFunctor
altogether.There’s a bunch of work to do:
Document the homomorphism parameter. Why is it a homomorphism? What does that mean? What should callers pass? (Why are you callingNot going to bother with this in this PR since Distributive Algebra #361 replaces it anyway.alg
yourself, anyway?) What should instances do with it?handleCoercible
.HFunctor
and its generic derivation machinery.