tomjaguarpaw / bluefin

MIT License
52 stars 6 forks source link

Difference to effectful #2

Open maralorn opened 6 months ago

maralorn commented 6 months ago

Hey there!

I am really intrigued by bluefin, it looks very cool. I am however a bit confused but the design and was hoping to for some clarifying discussion.

The handler as function arguments pattern is indeed very expressive and simple. However it immediately triggers a: So many function parameters, maybe this would be easier with a Reader? Which kinda defeats the purpose. I have a few questions about that:

Thank you for indulging me.

tomjaguarpaw commented 6 months ago

Hi @maralorn, thank you for your interest in Bluefin and your good questions! I suspect similar questions are going to come up a lot, so it's helpful for me to practice my answers.

So many function parameters

I think in practice there won't be so many function parameters. Users will bundle parameters that are commonly used together into product types (just like we do with normal function parameters).

maybe this would be easier with a Reader? Which kinda defeats the purpose

I'm not sure it defeats the purpose. I think the ideal situation would be to have the explicit Bluefin style and the implicit effectful style available in the same library, if the cost of switching between the styles can be kept low. I think it's plausible that an effectful style API could be layered on Bluefin. I think it's less likely that Bluefin style API could be layered on top of effectful (because it would be hard to convince effectful to have two effects of the same type in the same operation) but perhaps I've missed something.

passing handles around as values feels runtime heavy, can that be a performance problem?

I haven't benchmarked, but I doubt there's a performance problem passing handles around. Passing arguments is one of the cheapest things you can do, requiring no allocation. It's also the kind of thing that's very easy to eliminate with inlining. I certainly don't see how it can be more expensive than the closest alternative: passing arguments implicitly in a class constraint.

Since the handlers should be known at compile time could we make the the handler parameters type level arguments? e.g. with visible forall in 9.10?

I don't understand the connection between knowing the handlers at compile time and making them type level arguments. Type level arguments need not be known at compile time!

In any case, I have tried something similar but it seemed less ergonomic to me. You need TypeAbstractions in function arguments to make it remotely tractable, since you need to bind the handle in the argument to each handler, and we don't have those yet (I'm not sure about 9.10). Then you need to provide the argument as an explicit type application at each call site. (You may have been hoping to avoid that!)

There is an another alternative approach to implement an effectful style API on top of Bluefin, but I haven't fully worked it out yet.

once we have that, can't we try to infer the type arguments by making them implicit? Worst case that doesn't help because GHC can never infer the type, but maybe it can?

Yes, I guess you could do that. Then indeed GHC can infer the types (but, to reiterate, you need to apply the type applications explicitly at each call site, so it probably doesn't help you do what you want.)

The alternative approach could infer the types if they are unique ...

Once we are there, isn't that quite similar to effectful? But bluefin seems to be more potent in the kind of effects which can be implemented. Is that really because of the termlevel handlers or is there something else at play?

The alternative approach would be quite similar to effectful. In fact it would be so similar that it would be Bluefin redundant, unless you want to mix-and-match Bluefin and effectful styles, as I described above.

I don't think Bluefin is more "potent" in terms of effects that can be implemented. Despite saying in Bluefin's introduction that I don't know how to support Coroutine in effectful, @lysxia showed me how. I suspect something like Compound is possible too.

The big difference is that Bluefin makes effect disambiguation a non-issue. Already, having to disambiguate effects by type is a big ergonomic drag (effectful's author suggests newtypes when you want to use two states of the same type, for example) but there seems to be something about coroutines that requires type annotations even when there should be no ambiguity (I think it might be the fact that the effect has two type parameters).

Because Bluefin has no difficulties with disambiguation (the "cost has already been paid" by making handles explicit) it means it can be extremely flexible in other ways, for example Stream a is a synonym for Coroutine a (), EarlyReturn is a synonym for Exception and Jump is a synonym for EarlyReturn (). That kind of flexibility is hard to achieve with any system that uses types for disambiguation. Each of those effects would have to be a newtype (and with Jump it's not even clear what you would newtype: it doesn't have a type parameter!). That's very heavyweight.

To conclude this section I'll point out that I'm not convinced that explicit effect handles are actually a cost, in ergonomic terms. After using them for a while I actually prefer them to implicitly passed effects.

Thank you for indulging me.

You're welcome. I'm happy to help. Please do continue to ask any questions you may have or share any comments.

maralorn commented 6 months ago

I guess I’ll just try it out and see how it goes.

tomjaguarpaw commented 6 months ago

Great, please report your feedback!

Lysxia commented 6 months ago

I've been thinking about this area for a while too so thanks for starting this discussion!

passing handles around as values feels runtime heavy, can that be a performance problem?

Under the hood, effectful also passes handles around, since effectful's monad is a ReaderT Handle IO. The handles are being passed as an array, and adding a handler on the stack copies the array.

In bluefin, there is no implicit array/record of handlers. All of the handler passing is explicit.

A simple illustrative situation is two handlers on top of a call to the outer handler.

-- effectful: Eff es a = Env es -> IO a
handler1 :: Eff (E1 : es) B -> Eff es C
handler2 :: Eff (E2 : es) A -> Eff es B
call :: E1 :> es => Eff es A

example :: Eff es C
example = handler1 (handler2 call)
-- bluefin: Eff ss a = IO a
handler1 :: (E1 s -> Eff (s : ss) B) -> Eff ss C
handler2 :: (E2 s -> Eff (s : ss) A) -> Eff ss B
call :: s :> ss => E1 s -> Eff ss A

example :: Eff es C
example = handler1 (\h1 -> handler2 (\_ -> call h1))

A remark on ergonomics: if the effects E1 and E2 are identical, then the effectful example above changes semantics: call will access the inner handler. Maybe the average user of effectful only ever uses application-specific effects so they're always distinct. Personally, I ran straight into a situation with duplicate effects on the stack when I adapted bluefin's example code with coroutine.

For some related literature, Bluefin is similar to the named handlers of the Koka language, a feature presented in the paper "First-class names for effect handlers". Koka supports both unnamed handlers and named handlers, and uses the same rank-2 type trick as bluefin to keep track of the scope of names/handles.

Named handlers are a form of capability, and there's work exploring that connection implemented in the Effekt language.

maralorn commented 6 months ago

Okay, I will just continue abusing this thread for my questions. Please feel free to redirect me, e.g. to the Haskell discourse or anywhere else.

  1. I fear a bit off more than I can chew, by trying to convert an mtl style effect. The provided handler has the signature:

    runMyMonad :: forall r. (forall m. (MyMonad m, MonadIO m) => m r) -> IO r

    I then would like an effect with a method something like

    runInMyMonad :: (e :> es) => MyMonadHandler e -> (forall m. MyMonad m => m r) -> Eff es r

    At first I thought I could do this in effectful (because what I am trying is similar to the Handler example in the effectful docs. But I think that example is to specific. Do you know of a way I can do this (in effectful or bluefin)? (Full disclosure the real world runner I am trying to implement is reflex).

  2. While pondering the different ways bluefin implements handlers I noticed that I can’t find something akin to the dynamic dispatch concept with different runtime choosable interpretations like in effectful. All the handlers I looked at seem quite "Static". Do you think bluefin can also support that usecase? (which is often brought as a selling point for testing, etc.).

Lysxia commented 6 months ago

For adapting an existing mtl-style effect, have a look at the Dispatch modules (static or dynamic): https://hackage.haskell.org/package/effectful-core-2.3.0.1/docs/Effectful-Dispatch-Dynamic.html#g:4 (Edited out: code that was wrong. You need something more involved like what tom suggests in this comment below https://github.com/tomjaguarpaw/bluefin/issues/2#issuecomment-2034018715)


I believe that "dynamic effects" as they are called in effectful are those where the interface of the effect is encoded as a (G)ADT. For example, the State effect in the bluefin library is "static" because get and put are already specialized to readIORef and writeIORef, and there is no way to change that interpretation after the fact. For a dynamic state effect, you define the following GADT:

data StateOp s a where
  Get :: StateOp s s
  Put :: s -> StateOp s ()

Then the type of "handle"/"environment" that is being passed around can be exactly the handlers of those operations

data State s e = UnsafeState (forall a. StateOp s a -> IO a)  -- Instead of Bluefin.State.State

You now have all the freedom to give new interpretations to Get and Put.

An additional "dynamic" ability that you get in effectful is to override an existing handler using impose. This is possible because handlers can mess with all handles of existing handlers on the stack. In bluefin, you could have a primitive to create new handles for effects that are already on the stack:

newStateHandle :: e :> es => (forall a. StateOp s a -> Eff es a) -> (State s e -> Eff es a) -> Eff es a

then the equivalent of impose is to modify the handled computation to use that new handle instead of the old one.

maralorn commented 6 months ago

Thank you @Lysxia for your extensive example. That makes a lot of sense to me.

Sadly I underspecified the problem I am trying to solve a bit. In your example runMyMonad gets invoked newly everytime call is used. But the real runMyMonad I want to deal with has some kind of initialisation and state between calls and thus I need to use the same runMyMonad call for all call uses. The longer I think about it the more I believe that is only possible if MyMonad also implements MonadUnliftIO

tomjaguarpaw commented 6 months ago

I will just continue abusing this thread for my questions.

It's fine by me to continue here.

I fear a bit off more than I can chew, by trying to convert an mtl style effect. The provided handler has the signature:

runMyMonad :: forall r. (forall m. (MyMonad m, MonadIO m) => m r) -> IO r

I don't think I understand clearly enough what you're asking. I understand that you are using a library that has a function of this type (and the reflex example seems to confirm that). Are you asking how to run a Bluefin program with this handler? I guess not, because the higher-order argument to runMyMonad must be fully polymorphic in its monad. Are you asking how you would implement runMyMonad in a "Bluefin way", to see the alternative approach?

maralorn commented 6 months ago

I was hoping to use runMyMonad to define and run an effect with bluefin. But I assume that won’t be possible without introspecting what MyMonad really does and possibly replacing/reimplementing it.

In the end I would take any solution which would make reflex compatible with bluefin or effectful. The effectful documentation made me hope that this might be possible (because it gives so many examples of being compatible with a lot of stuff) but I currently don’t see how this is achievable.

tomjaguarpaw commented 6 months ago

I was hoping to use runMyMonad to define and run an effect with bluefin.

You won't be able to use runMyMonad for anything to do with Bluefin or effectful. Its type, (forall m. (MyMonad m, MonadIO m) => m r) -> IO r, implies that the only thing you can use when you define your m r is that it is MyMonad and MonadIO. You can't even use MonadState m, say, let alone m ~ Eff es.

On the other hand it's likely there are either primitives or internal Reflex functions that would allow you to do what you want. It looks like runHeadlessApp instantiates m to a composition of things like TriggerEventT, PostBuildT, ... . If you can expose a function that runs that concrete stack, and if all the monads in the stack are basically ReaderT r IO, then you'll be able to write a Bluefin- (or effectful-) compatible interface.

maralorn commented 6 months ago

Yeah, I realize that I will really need to dig into that stack to figure out if I can mirror it.

Another thought which came to mind: Have you thought about using Bluefin with ImplicitParameters for the handles? I haven’t used it and it is apparently not a very popular extension but it might be particularly useful for this?

tomjaguarpaw commented 6 months ago

Have you thought about using Bluefin with ImplicitParameters for the handles?

Yeah, it doesn't seem to work terribly well:

https://github.com/tomjaguarpaw/bluefin/commit/d55a7c9dc0524fb3ffec3ff68c57547b16c698aa

maralorn commented 6 months ago

On 2024-04-03 05:44, tomjaguarpaw wrote:

Yeah, it doesn't seem to work terribly well:

https://github.com/tomjaguarpaw/bluefin/commit/d55a7c9dc0524fb3ffec3ff68c57547b16c698aa

Aww, sad.

maralorn commented 6 months ago

Just fyi, I tried to play around with this a bit and I figured out how to define and run a Reflex effect. Its just the first step of the rather intricate transformer stack used in reflex, but with the POC working I think it will be possible.

You can find my example here: https://code.maralorn.de/maralorn/config/src/commit/646fb8833c181fd16a3c486b8a876c85f861f654/packages/kass/lib/Bluefin/Reflex.hs

tomjaguarpaw commented 6 months ago

Thanks to your prompting I wrote up an example of how to do dynamic effects with Blufin. It's really simple! (Although it could do with some ergonomics massaging.) You basically just create a record of operations, and then define them by delegating to other handlers.

https://github.com/tomjaguarpaw/bluefin/blob/b4a4853ca9e7ccc5aaa547465880a3b7aaf580c3/bluefin-internal/src/Bluefin/Internal/Examples.hs#L267-L325

maralorn commented 6 months ago

Nice! Just the weakenEff has feels a bit unergonomic. Also I realize how "methody" or just "namespacy" this can feel if you do fs.readFile. That’s pretty neat.

Generally at some point in the future a guide to the different effects manipulation operators like weakenEff, mergeEff would be awesome. But maybe best to wait a bit to discover which patterns are most common and useful.

On 2024-04-04 06:18, tomjaguarpaw wrote:

Thanks to your prompting I wrote up an example of how to do dynamic effects with Blufin. It's really simple! (Although it could do with some ergonomics massaging.) You basically just create a record of operations, and then define them by delegating to other handlers.

https://github.com/tomjaguarpaw/bluefin/blob/b4a4853ca9e7ccc5aaa547465880a3b7aaf580c3/bluefin-internal/src/Bluefin/Internal/Examples.hs#L267-L325

-- Reply to this email directly or view it on GitHub: https://github.com/tomjaguarpaw/bluefin/issues/2#issuecomment-2037200610 You are receiving this because you were mentioned.

Message ID: @.***>

tomjaguarpaw commented 6 months ago

Just the weakenEff has feels a bit unergonomic. ... Generally at some point in the future a guide to the different effects manipulation operators like weakenEff, mergeEff would be awesome. But maybe best to wait a bit to discover which patterns are most common and useful.

Yeah exactly, it is unergonomic, but I'm sure in time we'll work out an ergonomic way of doing it.

Also I realize how "methody" or just "namespacy" this can feel if you do fs.readFile. That’s pretty neat.

Ah yes! It looks very nice with OverloadedRecordDot. I didn't think of that.

tomjaguarpaw commented 5 months ago

This is a bit nicer now. You only need inContext and useImpl.

https://github.com/tomjaguarpaw/bluefin/blob/333cd61b863f52ec25683989276e4043f1def9d8/bluefin-internal/src/Bluefin/Internal/Examples.hs#L267-L334

maralorn commented 5 months ago

Nice, although the evalState () is confusing me a bit.

I do have by-the-way now a full blown working reflex effect going and I think I can pull of the same for reflex-dom. I am very pleased. (Progress here: https://code.maralorn.de/maralorn/config/src/branch/main/packages/kass/lib/Bluefin/Reflex.hs)

tomjaguarpaw commented 5 months ago

Nice, although the evalState () is confusing me a bit.

Oh, that's just a dummy handler to prove to myself that what I'm doing works in general, not just when there's only one handler.

I do have by-the-way now a full blown working reflex effect

Great!

tomjaguarpaw commented 5 months ago

I've got something I'm content with now. Here's a progressive worked example of a "counter" effect, followed by the filesystem effect. I'll write this up as a page in the Haddocks.

https://github.com/tomjaguarpaw/bluefin/blob/b5459db335bda44bbcb5be568002d08f8278983e/bluefin-internal/src/Bluefin/Internal/Examples.hs#L267-L489