Is the author mixing up “effect systems” with “algebraic effects”?
My understanding is that “algebraic effects” are the dynamic feature, almost like resumable exceptions, while “effect systems” are the static feature where side effects are checked in the type system.
The linked post about React is talking about algebraic effects, not an effect system.
On the other hand I’d say Java has an effect system (checked exceptions!) but not really algebraic effects.
The author wants to look up “type variance”. They will find that effects are intuitively contravariant, just like functions.
For those unfamiliar with the terminology, here two examples:
Lists are covariant. A list of apples is a list of fruits. When you are supposed to provide a list of fruits you can provide a list of apples.
Functions (and also effects) are contravariant. A function that works with any fruit as input will work with an apple as input (mind the reversed logic compared to lists).
Hence, when you are supposed to provide a function that works with an apple, you can provide a function that works with any fruit.
The same applies to effects and their combinations.
This is very cool but it is odd to me that I/O reading is not considered an effect. For one, it’s not idempotent and also you have no idea what reading the file could do. You could be reading off a FUSE filesystem that makes a network request or something.
Yep this was the strangest thing to me too. Even if IO reads don’t “mutate state” for some interpretation of state, they are not referentially transparent, which definitely means they’re not pure in my mind!
I rephrased it in my mind by “for the sake of simplicity let’s assume I/O reads are not effects”, that’s the only way I could make sense of it. Especially given the justification of why I/O writes are effects, which I’m fairly convince applies to reads as well.
Okay, this comment is pretty long, but I’ve been thinking about something similar to this for a few months now. I think something in this design space could provide a lot of the control that pure functional languages provide, but with less overhead (runtime, compile time, and mental).
Background
Monads encode effects, but they also encode the sequencing of those effects. They do this by reifying the effects as data structures and wrapping the values they are tracking. The runtime is able to interpret those effects and execute them. This allows the language to perform fine grained tracking over the effects and enable optimizations that might not be possible if there were fewer guarantees. This also gives the programmer confidence that if the effect isn’t part of the type system then it isn’t possible (modulo unsafe functions).
However, this means that there is a lot of wrapping and unwrapping of values, which has compile time costs, runtime costs, and general mental overhead costs. The effects are also not as composable because of the sequencing. E.g., a maybe io is not the same thing as io maybe.
Idea
If you weakened the monad by removing the sequencing property, I think you would get something like a set. Tracking effects in a set based manner would give you less information/power, but more composability. You could also take advantage of set based operations to restrict the effects that your children are allowed to call. For example, if we extend the syntax from the article with a block that explicitly chooses the effects that it’s contents have access to we could get something like this:
This would give the programmer the power to project guarantees down the stack without polluting the value space.
If you moved the values out of the runtime and into a parallel set based type system like the article suggests, then you could unwrap the values and thin out the language runtime. Since the values are simpler, the compiler would need to do less work to optimize the code and the optimizations would probably be less fragile. Since you don’t need the runtime to interpret the effects, you can just perform them, which might be faster. Additionally, FFI would probably be simpler with a thinner runtime allowing easier access to established ecosystems.
Differences from the Article
The article notes that these annotations could be a lot of work and I agree. However, I disagree on the solution: I think that effects should be inferred/unspecified by default. Obviously, there are exceptions to this:
The public API of a library
Areas of a code base that need effects specified for logic and/or correctness reasons
The second exception is a bit vague so let me elaborate with a few examples:
A web service probably doesn’t need to specify all of its effects.
A one off script/command line app probably doesn’t need this level of rigor either.
A build system might need to specify the allowed effects in some areas to prevent different kinds of attacks.
An embedded DSL might need to specify the allowed effects to prevent arbitrary code from being executed.
So the more you are in the end user realm, the less you need to define the effects. However, you can still benefit from the tracking: the compiler can probably put all of the effects in the docs so people can learn more about the application.
I have more thoughts on how this compares to effect systems, the article, specific properties I would like to see, and more, but I don’t want to find out what the comment length limit on lobste.rs is.
This looks pretty similar to what is done in Koka, for example. See maybe Lightweight Monadic Programming in ML (Nikhil Swamy, Nataliya Guts, Daan Leijen and Michael Hicks, 2011) for an academic description of the problems you are pointing out when mixing monads together.
Koka is very much the inspiration. I want the effect types system either without the effect system handlers or with the effect system handlers as an implementation detail of the language. I think the dynamic style of effect handlers might be too complex of a programming pattern for broad industrial adoption unless there is a good API to encapsulate it.
Just to clarify: for me “effect system” usually mean “static discipline / static checking for effects”. I think that you are using it to mean “effect handlers”, which is a specific approach to user-defined effects. If you mean that it makes sense to have static typing of effects without user-defined effects (though monads, effect handlers or something else), then I certainly agree.
But note that in practice people have found it fairly difficult to design static effect systems that are not too painful to use in practice. Java checked exceptions are widely considered to be a disaster, and there are few examples of successful large-scale adoption that people feel happy about. (Koka gets deserved praise; I find the work around capabilities in the Scala community also fairly interesting.) On the other hand, adding effect handlers without static checking, as a niche programming feature, is somewhat easier. (It is not easy, but then plenty of languages have toyed with delimited control in various forms before: call/cc, yield, coroutines, etc.)
You are absolutely correct. I meant effect handlers. The terminology trips me for some reason.
Java checked exceptions are widely considered to be a disaster, and there are few examples of successful large-scale adoption that people feel happy about.
This is why I think defaulting to inferred effects is important. The user should not be burdened with annotating every function with the effects it uses. It should only be specified where a contract is being enforced.
I also think that something like structural typing/row polymorphism might be useful. Rather than specifying all the effects that are present, being able to specify at least effects X, Y, Z must be present would be preferable. Or, if you think about set operations, being able to say at least not containing effects X, Y, Z.
I think that combination of inference and using set operations might make the system more flexible and ergonomic than systems like Java’s checked exceptions.
Just imagine how intricate optimization algorithms could get with knowledge of effects like this!
One effect that people don’t talk about often is potential non-termination. I’d imagine knowing that a function always terminates could be useful for optimising.
Speaking of going beyong type systems, why stop at effects? You could have the type system statically check that your function runs in polynomial time (see Linear Types and Non-Size-Increasing Polynomial Time Computation by Martin Hofmann (1999)).
Flix [1] looks like, based on my understanding of both, it has what the author describes as a static effect system. It doesn’t look like it has a dynamic one outside of Java interop [2].
The author is correct that effects can be made separable from types, provided you are careful to restrict inference such that types may inform effects but never vice-versa, even in the presence of complex type system features like typeclasses/traits and type projection. I’ve implemented this in my own language. Like Rust lifetimes, their inference can be left to a later pass and doing so does simplify the design of the type solver somewhat. However, like lifetimes, they are still statically inferred terms and, as such, I still like to think of them as ‘part of the type system’.
I think a better approach is reified capabilities combined with linear types. To make this ergonomic/practical, though, you also would want borrow checking and implicity-passed contexts, and likely context requirement inference.
All of this combined would allow typical imperitive code, but with statically inferred “effects” like “does IO”, “reads from a global”, “writes to a global”, all of which would be reified into types/traits, which means the language doesn’t need to know the possible “effects” when the language is designed, they can be defined in a host platform layer (a la Roc) or “unsafe” super-language (a la Rust).
This also means you could refine capabilities arbitrarily, i.e. “reads from this particular global memory within this lifetime bound”.
This wouldn’t really address function totality checking, though, unless you prevented any operations that could lead to an infinite loop by default and required a capability for things that could loop. So you would still need a separate totality checker, although it could be integrated into this system as another capability.
Is the author mixing up “effect systems” with “algebraic effects”?
My understanding is that “algebraic effects” are the dynamic feature, almost like resumable exceptions, while “effect systems” are the static feature where side effects are checked in the type system.
The linked post about React is talking about algebraic effects, not an effect system.
On the other hand I’d say Java has an effect system (checked exceptions!) but not really algebraic effects.
The author wants to look up “type variance”. They will find that effects are intuitively contravariant, just like functions.
For those unfamiliar with the terminology, here two examples:
Lists are covariant. A list of apples is a list of fruits. When you are supposed to provide a list of fruits you can provide a list of apples.
Functions (and also effects) are contravariant. A function that works with any fruit as input will work with an apple as input (mind the reversed logic compared to lists). Hence, when you are supposed to provide a function that works with an apple, you can provide a function that works with any fruit.
The same applies to effects and their combinations.
Functions are intuitively contravariant in their argument types, but covariant in their return types.
Yes that’s correct / more precise.
This is very cool but it is odd to me that I/O reading is not considered an effect. For one, it’s not idempotent and also you have no idea what reading the file could do. You could be reading off a FUSE filesystem that makes a network request or something.
Yep this was the strangest thing to me too. Even if IO reads don’t “mutate state” for some interpretation of state, they are not referentially transparent, which definitely means they’re not pure in my mind!
I rephrased it in my mind by “for the sake of simplicity let’s assume I/O reads are not effects”, that’s the only way I could make sense of it. Especially given the justification of why I/O writes are effects, which I’m fairly convince applies to reads as well.
Okay, this comment is pretty long, but I’ve been thinking about something similar to this for a few months now. I think something in this design space could provide a lot of the control that pure functional languages provide, but with less overhead (runtime, compile time, and mental).
Background
Monads encode effects, but they also encode the sequencing of those effects. They do this by reifying the effects as data structures and wrapping the values they are tracking. The runtime is able to interpret those effects and execute them. This allows the language to perform fine grained tracking over the effects and enable optimizations that might not be possible if there were fewer guarantees. This also gives the programmer confidence that if the effect isn’t part of the type system then it isn’t possible (modulo unsafe functions).
However, this means that there is a lot of wrapping and unwrapping of values, which has compile time costs, runtime costs, and general mental overhead costs. The effects are also not as composable because of the sequencing. E.g., a
maybe io
is not the same thing asio maybe
.Idea
If you weakened the monad by removing the sequencing property, I think you would get something like a set. Tracking effects in a set based manner would give you less information/power, but more composability. You could also take advantage of set based operations to restrict the effects that your children are allowed to call. For example, if we extend the syntax from the article with a block that explicitly chooses the effects that it’s contents have access to we could get something like this:
Or maybe we want a block that excludes certain effects like this:
This would give the programmer the power to project guarantees down the stack without polluting the value space.
If you moved the values out of the runtime and into a parallel set based type system like the article suggests, then you could unwrap the values and thin out the language runtime. Since the values are simpler, the compiler would need to do less work to optimize the code and the optimizations would probably be less fragile. Since you don’t need the runtime to interpret the effects, you can just perform them, which might be faster. Additionally, FFI would probably be simpler with a thinner runtime allowing easier access to established ecosystems.
Differences from the Article
The article notes that these annotations could be a lot of work and I agree. However, I disagree on the solution: I think that effects should be inferred/unspecified by default. Obviously, there are exceptions to this:
The second exception is a bit vague so let me elaborate with a few examples:
So the more you are in the end user realm, the less you need to define the effects. However, you can still benefit from the tracking: the compiler can probably put all of the effects in the docs so people can learn more about the application.
I have more thoughts on how this compares to effect systems, the article, specific properties I would like to see, and more, but I don’t want to find out what the comment length limit on lobste.rs is.
This looks pretty similar to what is done in Koka, for example. See maybe Lightweight Monadic Programming in ML (Nikhil Swamy, Nataliya Guts, Daan Leijen and Michael Hicks, 2011) for an academic description of the problems you are pointing out when mixing monads together.
Koka is very much the inspiration. I want the effect
typessystem either without the effectsystemhandlers or with the effectsystemhandlers as an implementation detail of the language. I think the dynamic style of effect handlers might be too complex of a programming pattern for broad industrial adoption unless there is a good API to encapsulate it.Just to clarify: for me “effect system” usually mean “static discipline / static checking for effects”. I think that you are using it to mean “effect handlers”, which is a specific approach to user-defined effects. If you mean that it makes sense to have static typing of effects without user-defined effects (though monads, effect handlers or something else), then I certainly agree.
But note that in practice people have found it fairly difficult to design static effect systems that are not too painful to use in practice. Java checked exceptions are widely considered to be a disaster, and there are few examples of successful large-scale adoption that people feel happy about. (Koka gets deserved praise; I find the work around capabilities in the Scala community also fairly interesting.) On the other hand, adding effect handlers without static checking, as a niche programming feature, is somewhat easier. (It is not easy, but then plenty of languages have toyed with delimited control in various forms before: call/cc, yield, coroutines, etc.)
You are absolutely correct. I meant effect handlers. The terminology trips me for some reason.
This is why I think defaulting to inferred effects is important. The user should not be burdened with annotating every function with the effects it uses. It should only be specified where a contract is being enforced.
I also think that something like structural typing/row polymorphism might be useful. Rather than specifying all the effects that are present, being able to specify at least effects X, Y, Z must be present would be preferable. Or, if you think about set operations, being able to say at least not containing effects X, Y, Z.
I think that combination of inference and using set operations might make the system more flexible and ergonomic than systems like Java’s checked exceptions.
Is this similar to Koka’s effect system?.
One effect that people don’t talk about often is potential non-termination. I’d imagine knowing that a function always terminates could be useful for optimising.
Speaking of going beyong type systems, why stop at effects? You could have the type system statically check that your function runs in polynomial time (see Linear Types and Non-Size-Increasing Polynomial Time Computation by Martin Hofmann (1999)).
Maybe also the optimizer could recognize that some effects have some of the guarantees of pure code:
Flix [1] looks like, based on my understanding of both, it has what the author describes as a static effect system. It doesn’t look like it has a dynamic one outside of Java interop [2].
[1] https://doc.flix.dev/effects.html [2] https://doc.flix.dev/exceptions.html
The author is correct that effects can be made separable from types, provided you are careful to restrict inference such that types may inform effects but never vice-versa, even in the presence of complex type system features like typeclasses/traits and type projection. I’ve implemented this in my own language. Like Rust lifetimes, their inference can be left to a later pass and doing so does simplify the design of the type solver somewhat. However, like lifetimes, they are still statically inferred terms and, as such, I still like to think of them as ‘part of the type system’.
I think a better approach is reified capabilities combined with linear types. To make this ergonomic/practical, though, you also would want borrow checking and implicity-passed contexts, and likely context requirement inference.
All of this combined would allow typical imperitive code, but with statically inferred “effects” like “does IO”, “reads from a global”, “writes to a global”, all of which would be reified into types/traits, which means the language doesn’t need to know the possible “effects” when the language is designed, they can be defined in a host platform layer (a la Roc) or “unsafe” super-language (a la Rust).
This also means you could refine capabilities arbitrarily, i.e. “reads from this particular global memory within this lifetime bound”.
This wouldn’t really address function totality checking, though, unless you prevented any operations that could lead to an infinite loop by default and required a capability for things that could loop. So you would still need a separate totality checker, although it could be integrated into this system as another capability.