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

Add support for custom handles #14

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open

Conversation

Lysxia
Copy link
Contributor

@Lysxia Lysxia commented May 6, 2024

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 those HFunctor 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".

@tomjaguarpaw
Copy link
Owner

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 MyReader effect in terms of State (it just gets the current value of the State)? (I appreciate that's not really a "reader" any more, but for the sake of argument ... .)

Comment on lines 46 to 48
-- | 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
Copy link
Owner

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.

Copy link
Contributor Author

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?

Copy link
Owner

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.

Copy link
Owner

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!

Copy link
Contributor Author

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)

Copy link
Owner

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

Copy link
Owner

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!

@tomjaguarpaw
Copy link
Owner

Could you add an example to confirm this works when the handler is implemented in terms of another effect?

In fact, if you can demonstrate runFileSystemPure using this technique, that would be ideal.

@tomjaguarpaw
Copy link
Owner

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 insertSeconds, but otherwise this looks good.

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

@arybczak
Copy link

arybczak commented May 7, 2024

local is missing.

@tomjaguarpaw
Copy link
Owner

Let's continue the discussion about higher-order effects at #15.

@tomjaguarpaw
Copy link
Owner

Very nice, thanks! I'll have a bit of a play, implement some other example with it, and rewrite Bluefin.Compound.

@tomjaguarpaw
Copy link
Owner

(I took the liberty of rebasing on master and fixing a conflict.)

@tomjaguarpaw
Copy link
Owner

I'm still waiting for a time when I can look at this properly, but it's one of my highest priority items.

@tomjaguarpaw
Copy link
Owner

Wow, I found a very nice way of defining this style of handler within any level of enclosing handlers! (within). I think this is very close to becoming the "official" way of creating new effects. Before that I need to confirm it's compatible with #17.

(and rebased on master)

Comment on lines 626 to 628
-- exampleRunFileSystemPure' :: Either String String
-- exampleRunFileSystemPure' = runPureEff $ try $ \ex ->
-- runFileSystemPure' ex [("/dev/null", "")] action'
Copy link
Owner

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.

Copy link
Contributor Author

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 of Handle), we do some HasField 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

Copy link
Owner

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

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.

3 participants