Hacker News new | ask | show | jobs
by brabel 12 days ago
A real effect system allows you to do things like NOT continue execution after using the effect (like the error effect does - if you "implement" this by using Exceptions, you're not using effects at all, just using Exceptions with extra steps) or only continuing it after some asynchronous work happens (the Future effect), or even "continue" execution several times. That just cannot be done with "just passing stuff in". You still don't seem to have understood effects.
1 comments

Thanks for your response. Perhaps I'm missing some fundamental things. Could you help?

> A real effect system allows you to do things like NOT continue execution after using the effect

Right, Bluefin's Request allows you to do that too. For example here is an example of handling the request by continuing or not, depending on what the value yielded to the Request is.

    example :: Either String ()
    example = runPureEff $ try $ \ex -> do
      forEach
        ( \r -> do
            request r True
            request r True
            request r False
            request r True
            request r True
        )
        ( \case
            True -> pure ()
            False -> throw ex "Stopped"
        )
> if you "implement" this by using Exceptions, you're not using effects at all, just using Exceptions with extra steps

Not sure I follow that. Above you can see I used an exception (Bluefin's Throw capability), but I couldn't have used only an exception because that would have aborted unconditionally. What am I missing here, that makes "using Exceptions" "not using effects at all"?

> only continuing it after some asynchronous work happens (the Future effect)

I'm not really sure what "a Future effect" is, but I don't see how it's not something that can be run as a function call, at least in Haskell.

> or even "continue" execution several times

Right, these are the multishot continuations which Bluefin doesn't support. I haven't discovered many particularly compelling use cases for multishot continuations but would be very interested in finding some. The developer of the Kyo effect system for Scala, Flavio Brasil, suggested parsing, with multiple parse results, which makes sense.

I'm also not entirely sure Bluefin couldn't simulate common use cases of multishot continuations with threads, but I haven't thought about it very hard.

> You still don't seem to have understood effects.

Possibly true, and part of my puzzlement! I'm always happy to try to improve my understanding. Can you help me see what I've missed?

I think the parent may be getting at the continuation aspect of effects? Effect systems make the stack a first class object you can reuse, I think a standard example is implementing a scheduler. I'm not familiar with your Bluefin library so maybe it already handles this:

  effect Sched =
    yield : unit -> unit
    fork  : (unit -> unit) -> unit
  end
  
  let mut run_queue = []
  let enqueue t = run_queue := List.concat run_queue [t]
  
  let dequeue () =
    match run_queue with
    | [] -> ()
    | t :: rest ->
      run_queue := rest;
      t ()

  let rec spawn task =
    handle
      task ()
    with
    | return _ -> dequeue ()
    | yield () k ->
      enqueue (fn () -> resume k ());
      dequeue ()
    | fork f k ->
      enqueue (fn () -> resume k ());
      spawn f

  let run main = spawn main

  let worker name steps =
    let rec loop i =
      if i > steps do ()
      else do
        print $"{name}: step {i}";
        perform yield ();
        loop (i + 1)
      end
    in
    loop 1
  
  let () =
    run (fn () ->
      print "main: starting";
      perform fork (fn () -> worker "A" 3);
      perform fork (fn () -> worker "B" 3);
      print "main: forked workers, now yielding";
      perform yield ();
      print "main: done")
output:

  main: starting
  A: step 1
  B: step 1
  A: step 2
  main: forked workers, now yielding
  B: step 2
  A: step 3
  main: done
  B: step 3
Ah yes, OK, I missed the point that the timeout is applied to the entire continuation, not just the part of the computation until the next await. Bluefin can't currently do that. I think I could make it do that, using the same implementation strategy as awaitYield (fork a thread, communicate through an MVar) but I wonder what the point is, given that Bluefin allows you to run the continuation at most once. Is the use case of "run the continuation in a modified environment (e.g. with a timeout)" really that compelling? Maybe it is! But I don't see it yet.

On the other hand, I don't see any difficulty with implementing a scheduler using Await/Yield. I don't think it needs access to the full continuation.