And I would like to thank you for two things which were essential components in the development of Bluefin: firstly developing effectful
itself, and demonstrating that a well-typed IO
-based effect system was possible, and secondly, your comment on Reddit that made me realize that that was the correct approach for Bluefin too, not delimited continuations.
Perhaps misleading, but as you say, out of the box it is indeed not possible. I think every effect system should provide lightweight streaming as a first class citizen. It is a wonderfully-powerful abstraction!
I think it is easy, and I think Bluefin does it. There are some features it does not provide (those based on consuming elements from a stream individually – but then again those are not safe anyway, in streaming
etc. You need linear types to make them safe.) but it does provide 90% of what you want from a streaming API, and even reimplements 90% of the pipes
API directly!
In fact, what Bluefin provides is even better than streaming
and pipes
because Bluefin streams are code, not data: no risk of accidentally materializing your stream and getting a space leak.
Interesting to know. I wonder if a plugin could also help in the cases where Bluefin type inference breaks down. I’ll have to look into it.
Ah, that’s really understating the case! Suppose I want a (static) compound effect that combines state and exceptions. In Bluefin I just write a data type to do that:
data Combined e =
MkCombined (State Int e) (Exception String e)
Or suppose I want a dynamic effect that supports writing Int
s and reading String
s. Then I just define
data Combined2 e =
MkCombined2 (Int -> Eff e ()) (Eff e String)
That’s it! There were things that I wanted. I wrote them.
Now how do I do the same in effectful
? The first, well, I’m not really sure I can. Can I? I can do
type Combined e = (State Int :> e, Exception String :> e)
but that leaks the internals. Maybe that’s OK. I could define a new class
class (State Int :> e, Exception String :> e) =>
Combined e
instance (State Int :> e, Exception String :> e) =>
Combined e
but I’m not sure that plays well with type checking.
The second, I have to do:
data Combined2
WriteInt :: Int -> Combined2 m ()
ReadString :: Combined2 m String
This is really very indirect! I didn’t want an ADT with WriteInt
and ReadString
fields. I just had to write them to get what I really wanted.
Granted, the Bluefin story isn’t as simple as I make out either, there’s a bit of massaging you have to do to get the types to work out. But it is much more direct than the effectful
story.
It’s because I find streaming so useful. I don’t understand how others can do without it. About half of the lists I create now, I create using streaming. I think until you have access to a lightweight streaming abstraction it’s hard to know just how useful it is.
Yes, fair enough. If you have 10 or 20 effects, and you don’t want to abstract them into coarser-grained effects, then Bluefin won’t help you. I personally haven’t come across this use case (in fact I’d work hard to not have it happen) but perhaps it’s a matter of taste.
Do you mean the question at around 1:01:00? I believe it was answered around 1:03:00. Or are there still issues left unresolved?
I still haven’t managed to understand what a “higher order effect” actually is. My current best guess is that a higher order effect is just a handler, but for path dependent reasons they have been conferred with special status. I haven’t yet come across a higher order effect that I couldn’t obtain just by using a handler. For example, a Bluefin “reader local” is just
import Bluefin.Eff
import Bluefin.Reader
local ::
e1 :> es =>
Reader r e1 ->
(r -> r) ->
(forall e. Reader r e -> Eff (e :& es) z) ->
Eff es z
local re f k = do
r <- ask re
runReader (f r) k
I have yet to understand why this is such a marvellous and important construction, but I will keep trying.
Either I have missed something, higher-order effects are terribly important and thus Bluefin is terribly flawed, or higher-order effects are really nothing special, people got tied up in knots chasing their tail about them (for path dependent reasons) and Bluefin is simpler because it doesn’t try to shoehorn them in. Of course the former is possible, but my hunch is that it’s the latter than pertains.
I haven’t worked on this because it hasn’t been particularly important to me. Perhaps it’s important to someone else. If so they can open an issue and I’ll make a nice story about it.
That’s said, MTL compatibility is important for converting legacy code to effectful
/Bluefin but is of basically no utility for new code. If you’re writing an MTL-style operation, and you use an effectful
/Bluefin handler, you are then immediately out of the MTL world forever. There’s no way of going back. Nor is there a way of using a native effectful
/Bluefin effect and going back into the MTL world. So I can’t imagine that there’s a huge demand for flexible MTL compatibility, and if there’s not then a newtype
escape hatch seems simple and good enough, for example:
newtype MyEffect r =
MkMyEffect (forall e. State s e -> Exception ex e -> Eff e r)
instance (MonadState s) MyEffect where ...
instance (MonadError ex) MyEffect where ...
This sounds very similar to me to making compound effects/effect abstraction. @maxigit says it “breaks encapsulation”, but to me it seems fine.
I think @maxigit explicitly does not want this, as explained below (using the terminology “breaking encapsulation”). Using Region
to abstract Bluefin’s State
effects is a sort of “coarse graining”. @maxigit wants “fine grains”. Personally, I don’t. But it seems some people (including CircuitHub, with their 20 grains) do.