Most active commenters
  • tome(3)

←back to thread

97 points appliku | 13 comments | | HN request time: 0.2s | source | bottom
1. vips7L ◴[] No.45159540[source]
Maybe I’m not getting it, but isn’t this just interfaces and implementations from the OO world? For example their movie one is:

    interface MovieApi {
        List<Movie> getPopularMovies();
    }
    
What are effects providing over this?
replies(5): >>45159701 #>>45159720 #>>45160356 #>>45162050 #>>45194243 #
2. iroddis ◴[] No.45159701[source]
I wasn’t aware of effects until I read the article, but I like the idea. The example you have is just an interface. Effects work a bit like boundary hooks. A pure function only works on things internal to its scope, and so it has no effects. An impure function might affect things outside of its scope, and may want to allow the caller to hook when those effects occur. Think a function that uses the gpu. That’s a side effect, and the function should be decorated to reflect that.

I’m guessing some languages would allow for checking to ensure that any induced effects are correctly noted and advertised so that the system as a whole can be more easily reasoned about.

3. abeppu ◴[] No.45159720[source]
I _think_ the distinction is that the interface tells you what MovieAPI _does_, but this system also tracks what are contextual requirements to do that thing. So when they write their version of the MovieAPI that calls out to an existing web api, they have to write `\ {Net, IO}` on the main method that uses them. When they write their test that uses a canned recommendation, they can just write `\ {IO}`, and they have a complicated pile of test logic and confirm just at a glance at the top-level signature that no network calls are made when the test runs.

So in java, you could write two distinct classes implementing the MovieApi interface as you defined it, one of which calls out to the web and one which doesn't, and nothing about the type indicates that difference. If you accidentally used the wrong class, your tests could make a network call and you might never notice, because you would not have tracked that effect.

For someone with a java background, it's really helpful to make the analogy to Checked Exceptions, which let us propagate information about what kind of handler must be required contextually.

replies(1): >>45160507 #
4. alethic ◴[] No.45160356[source]
It's similar on the surface. Another language, Effekt, does actually use interfaces for their effect declarations rather than having a separate `eff` declaration.

The difference comes in their use. There's two things of note. First, the implementation of an interface is static. It's known at compile time. For any given concrete type, there is at most one implementation of MovieApi. You're using the interface, then, to be generic over some number of concrete types, by way of only specifying what you need. Effect handlers aren't like this. Effect handlers can have many implementations, actually. This is useful in the case of ex. adding logging, or writing tests to simulate I/O without actually doing it, or just having different behavior at different places across the program / call stack...

    eff MovieApi {
      def getPopularMovies();
    }
    def main() {
      run {
        println("Alice's movies: ", getPopularMovies());
      } with handler MovieApi {
        def getPopularMovies() = [
          "Dr. Strangelove", 
          "Lawrence of Arabia",
          "The End of Evangelion",
          "I Saw the TV Glow"
        ];
      }
      run {
        println("Bob's movies: ", getPopularMovies());
      } with handler MovieApi {
        def getPopularMovies() = [
          "The Magic School Bus: Space Adventures",
          "Spy Kids 3-D: Game Over",
          "Twilight: Breaking Dawn: Part II"
        ];
      }
    }
Second, the effects of effect handlers are not functions. They're under no obligation to "return", and in fact, in many of the interesting cases they don't. The `resume` construct mentioned in the article is a very special construct: it is taking the "continuation" of the program at the place where an effect was performed and providing it to the handler for use. The invocation of resume(5) with a value looks much like a return(5), yes. But: a call to resume 1) doesn't have to happen and the program can instead continue after the handler i.e. in the case of an effectful exception, 2) doesn't have to be invoked and the call to resume can instead be packaged up and thunkified and saved to happen later, and 3) doesn't have to happen just once and can be invoked multiple times to implement fancy backtracking stuff. Though this last one is a gimmick and comes at the cost of performance (can't do the fast call stack memcpy you could do otherwise).

So to answer your question more briefly, effects differ from interfaces by providing 1) a decoupling of implementation from use and 2) the ability to encompass non-local control flow. This makes them not really compete with interfaces/classes even though the syntax may look similar. You'd want them both, and most effectful languages have them both.

5. alethic ◴[] No.45160507[source]
The checked exceptions analogy is a good one. Thinking of effect handlers as resumable checked exceptions with some syntactic sugar is very accurate. For someone with a Haskell background, thinking about them as "dependency injection" is also helpful (and these notions are equivalent!) but for the Java heads out there, yeah, resumable checked exceptions provides a really good mental model for what effect handlers are doing with the call stack in the general case.
replies(1): >>45165429 #
6. KPGv2 ◴[] No.45162050[source]
Implementing an interface results in a class that implements it. From then on, the fact your class implements that interface is entirely obscured from your code. In the case of tracking effects, this is clearly undesirable. (Also, in a language that doesn't have classes, implementing interfaces is difficult for me to think about.)

In the case of algebraic effects, they're functions with type signatures explicating the effects the function has. Those don't go away until somewhere nearer the edge of your program, outside the core, you say "by the way, use this to handle those effects"

Effect handlers also give you control over continuations so you can sequence effects explicitly, which is—speaking as someone who writes a lot of code in a language that has algebraic effects as a core part of the language (Unison)—really powerful. Deciding in some instance how you prioritize IO vs Exceptions, making a thrown error go away and replacing it with an HTTP effect that, wayyyy far away from this code handles all HTTP calls by attaching a self-signed certificate when doing the SSL handshake, or whatever, is very nice.

The more I write in this style, the more interesting techniques I come across.

replies(1): >>45162641 #
7. vips7L ◴[] No.45162641[source]
An interface is just a function signature. Perhaps it might be easier for you to think about as just a function:

    type MoviesApi = void => List[Movie]
I guess the difference is the tracking?
8. tome ◴[] No.45165429{3}[source]
What’s the difference between a resumable checked exception and a function call?
replies(1): >>45165741 #
9. saviorand ◴[] No.45165741{4}[source]
Function call always returns, and to one single caller, whereas effects can choose not to "return" at all, resume multiple times, etc
replies(1): >>45165987 #
10. tome ◴[] No.45165987{5}[source]
Right, though the former is just an exception. So what general effect systems provide above and beyond what we already have in most languages is "multiply-resumable" checked exceptions (also known as multi-shot continuations and often provided by "delimited continuations").

At the time I developed my Haskell effect system Bluefin there was a conventional wisdom that "you can't implement coroutines without delimited continuations". That's not true: you can implement coroutines simply as function calls, and that's what Bluefin does.

(The story is not quite as simple as that, because in order for coroutines to communicate you need to be able to pass control between threads with their own stack, but you still don't need multi-shot continuation.)

replies(1): >>45167883 #
11. saviorand ◴[] No.45167883{6}[source]
Good point! You might be interested in reading this article on the topic: https://without.boats/blog/coroutines-and-effects/
replies(1): >>45178726 #
12. tome ◴[] No.45178726{7}[source]
Thanks, I did find that interesting. I would say Bluefin is another entry in the static/lexical row, whereas its cousin effectful is in the static/dynamic row (although this may be a slightly different interpretation of the terms than is used in the article).
13. vjerancrnjak ◴[] No.45194243[source]
Flix does not have generic effects, yet it has generic traits.

This hints that effects are not meant to deal with data variability.

Effects are allowing variability of control flow.

Abstracting `try/catch/finally` or `async/await`.

For example, something like "durable workflows" is just a specific effect implementation, code stays the same.