-
Notifications
You must be signed in to change notification settings - Fork 8
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
Add support for custom handles #14
base: master
Are you sure you want to change the base?
Conversation
Ah, this is very interesting. I am looking for ways that we can make dynamic effect definition easier. Could you add an example to confirm this works when the handler is implemented in terms of another effect? For example, define the |
-- | Effect signatures must be instances of this class: they are higher-order functors. | ||
class CovariantSig (f :: Sig) where | ||
smap :: (forall x. m x -> n x) -> f m -> f n |
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 interesting. Compare:
smap useImpl :: (CovariantSig f, e :> es) => f (Eff e) -> f (Eff es)
mapHandle :: (e :> es) => h e -> h es
So they basically do the same job. class Handle
/mapHandle
has the benefit that the Eff
is rolled in to the definition, with the hope that there's less that can "go wrong" when trying to implement or use instances. CovariantSig
/smap
has the benefit of being "more standard", because it doesn't mention anything Bluefin-specific. I'm not sure what's preferable. The latter allows f
to be reused across different effect library implementations.
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 would be in favor of reusing what's already here. There are two things called Handle
now. Should we rename one?
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.
Yeah, I don't know which one though. Your one sounds like it deserves the name more. Perhaps the class should be called IsHandle
.
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.
Then we'd have something like instance IsHandle (Handle f)
which seems pretty reasonable!
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 just observed that there's stuff in Compound doing similar things as this PR! (last time I looked was the old interface)
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.
Yes, but yours is better because of
allows the record field selectors to be used directly
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 tried to support that by doing
data FooHandle e = { foo :: forall es. e :> es => Bar -> Eff es Baz }
but it wasn't ergonomic to define handlers that way. Your version is to use the simple handle definition
data FooHandle e = { foo :: Bar -> Eff e Baz }
and then put forall es. e :> es =>
on the outside. It works out much nicer!
In fact, if you can demonstrate |
Here's what it looks like to write a handler with more than one operation in the context of more than one other handler. We'll need an ergonomic way of dealing with repeated data MyReader r m =
MkMyReader { myAsk :: m r,
myAsk2 :: Int -> m r
}
instance CovariantSig (MyReader r) where
smap f h =
MkMyReader { myAsk = f (myAsk h), myAsk2 = fmap f (myAsk2 h) }
runMyReader ::
r ->
(forall s0. H.Handle (MyReader r) s0 -> Eff (s0 :& s) a) ->
Eff s ([Int], a)
runMyReader r k =
evalState r $ \st -> do
yieldToList $ \y -> do
with
(MkMyReader { myAsk = get st,
myAsk2 = \i -> yield y i *> get st })
(\rr -> insertSecond (insertSecond (k rr))) |
|
Let's continue the discussion about higher-order effects at #15. |
Very nice, thanks! I'll have a bit of a play, implement some other example with it, and rewrite |
(I took the liberty of rebasing on |
I'm still waiting for a time when I can look at this properly, but it's one of my highest priority items. |
Wow, I found a very nice way of defining this style of handler within any level of enclosing handlers! ( (and rebased on |
-- exampleRunFileSystemPure' :: Either String String | ||
-- exampleRunFileSystemPure' = runPureEff $ try $ \ex -> | ||
-- runFileSystemPure' ex [("/dev/null", "")] action' |
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.
Ah, but this is no good :( I get an ambiguous type error if I try to do this. So maybe we need to make Handle
a newtype? In which case we lose the nice property that we can use the record fields without further adjustment.
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.
Good catch. I should have guessed that Handle
works badly with type inference. Personally if that doesn't work I would rather go back to monomorphic records (i.e., replace Handle f e
with f e
and ask users to wrap method invocations with useImpl
, this is commonplace in other effect systems (perform
in OCaml, send
in effectful)) (but then we should reevaluate how this PR compares with the similar feature that's already in bluefin, because that Handle
trick was one of this PR's main selling points).
I'm not a fan of the following hacks, but they could be attempted if one really wanted to make Handle
work with type inference. The common idea is that we need the effect e
to appear not only in constraints in Handle
, but somehow as an index of a data type.
-
Hack 1: users should declare records with an extra phantom effect parameter:
data FileSystem e es = FileSystem { readFile :: String -> Eff es String, writeFile :: String -> String -> Eff es () } type Handle f e = forall es. e :> es => f e es
-
Hack 2: if we want users to not think about this hack,
Handle
should somehow introduce that phantom parameter. And in order to not lose the field accessors (because that's the whole point ofHandle
), we do someHasField
shenanigans.newtype TaggedHandle f e es = TaggedHandle (f es) instance HasField name (f es) a => HasField name (TaggedHandle f e es) a where getField (TaggedHandle r) = getField @name r type Handle f e = forall es. e :> es => TaggedHandle f e es
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.
Hack 1 looks OK! But I still can't get it to work. I get
src/Bluefin/Internal/Examples.hs:628:45: error:
• No instance for (e2 :> es) arising from a use of ‘action'’
Possible fix:
add (e2 :> es) to the context of
a type expected by the context:
forall (e2 :: Effects).
Handle FileSystem e2 -> Eff (e2 :& (ex :& es)) String
• In the third argument of ‘runFileSystemPure'’, namely ‘action'’
In the expression:
runFileSystemPure' ex [("/dev/null", "")] action'
In the second argument of ‘($)’, namely
‘\ ex -> runFileSystemPure' ex [("/dev/null", "")] action'’
|
628 | runFileSystemPure' ex [("/dev/null", "")] action'
| ^^^^^^^
I've pushed the code for that to the branch. (I introduced a new class IsHandle1
for these two-parameter things.)
Based on your suggestion on the cleff issue tracker re-xyr/cleff#31
I mainly wanted to show an implementation that allows the record field selectors to be used directly, so you don't need to declare operations as separate functions.
I specialized the name of the
CovariantSig
class, but of course it's just one of thoseHFunctor
elsewhere.As I mentioned on Discourse, this is an interface that is forwards-compatible with algebraic effects while being hopefully useful on its own as a restricted form of effect handlers ("tail-resumptive"). In effectful-core, this corresponds to the
Dispatch.Static
module.I tried to avoid the word "handler" (which can be too easily overloaded), instead sticking to the word "handle" as in "handle-pattern".