The issues with effect systems

Yes, but what you’re describing is still testing the effect implementations, not the effectful computations. I believe both the original comment by @tcard and I are referring to the advantage of effect systems concerning testing effectful computations.

About testing effect implementations, I really think we should prove the properties formally instead. Or maybe I’m wrong and we actually should test them, in which case what you described is an unsolved problem.

1 Like

I believe both the original comment by @tcard and I are referring to the advantage of effect systems concerning testing effectful computations.

In what way? Since effects can be represented with monadic types (as shown by Philip Wadler), I’m having difficulty trying to understand what benefits an effects system have over them - links to pertinent URLs would be helpful…

What I meant is that “by allowing the user to provide multiple implementations for one interface, we make mock testing possible.” Any system that has this property will be able to do mock testing, and common Haskell effect libraries happen to have this property. Does this answer your question?

What are effect system really for?

To me, there are two interesting properties:

  1. Effects are statically checked and I can constrain them to certain functions etc
  2. Effects are abstract and one can switch out the interpreter and run the same code (e.g. your data in memory instead of in postgres or a fake filesystem etc.)

Well, 1. doesn’t necessarily need to live in the type system (or in a single type system). An effects system as part of the language could be implemented in a number of different ways.

Point 2. is wildly exciting, but also wildly chaotic. We already have massive issues with type classes for this reason and try to come up with laws and properties that hold for all instances, so that we can at least do some reasoning. With effects and interpreters of arbitrary complexity and depth, this will easily make reasoning about effect code impossible, unless you have a specific interpreter in mind. Then the question is: why use an effects system at all?

Overall, I see their use cases, but I’m not sure we’re advanced enough yet to know when and how to use them. So I’m very skeptical of introducing them in production settings.

The times I’ve dealt with effect system code professionally, it made it really hard to figure out what the code is actually doing and not in an abstract sense. Tracking down the real implementation bits took a long time, jumping through loads of indirections.

4 Likes

“see what effects a block of code does not have”

There’s no way to do the above in anything that uses IO as a base type

Working with records-of-functions, we can restrict effects by parameterizing the records by a monad and being polymorphic over the monad: the effects must then come only from the dependencies. Like in the above-mentioned constructor:

makeRepository :: (Has Logger m deps, Has SomeOtherDep m deps) 
               => deps -> Repository m

The “methods” in Repository can’t do IO on their own. Of course, when wiring together the application, we might then commit to IO.

…but according to you, mock testing only tests the implementation of effects - by the responses of yourself and others, I thought there were other benefits I didn’t know about.


Again, if there’s a monadic type for each effect, mocking is still possible - what other benefits does an effects system provide?

I think you have misread. It tests the effectful computations, not the implementation of effects.

1 Like

FWIW I personally don’t find number 2 very exciting. The number of times I’ve wanted to switch out interpreters is very limited (perhaps zero). One might argue I’ve never done it because it hasn’t been easy before now, but I am skeptical.

For me 1 is the key property: I really, really want to know what effects parts of my programs can do and, probably more importantly, can’t do. That’s most of the reason I use Haskell.

2 Likes

Can you show me an equivalent example, in the records-of-functions setting, to the one I showed you? That is, throwing an exception (or exception-like thing), catching it, and having the fact that an exception was thrown internally be invisible externally (i.e. it’s not present in the type)?

That is, can you write something like:

bar :: Has (Exception String) m deps => deps -> m Int
bar = if True then throw deps "Hello" else pure 5

foo :: deps -> m (Either String Int)
foo _? = handleExceptionSomehow bar

I’m not aware of any record-of-functions approach that can do this currently.

(Personally I like the record-of-functions approach and I’m working on a version that can remove effects like this. If there’s already an effect system that can do that, so much the better, I’ll just use that! But I don’t believe there is.)

1 Like

One other advantage of effect systems is that they are like modular monads. Instead of having to write one monolithic monad from scratch for every application, you can simply compose it from existing effects.

Yes, mtl can also do things like that, especially if you use deriving-trans, but that approach is not really any easier than using effect systems in my opinion.

2 Likes

Hrm:

If you want to test the action, why would you want to use a mock implementation of the effect? Surely you would want to know that the action works with the real implementation…

Effect systems allow you to both study the a mock implementation and a real implementation. The mock implementation can simulate very rare events that might never occur when testing the real implementation (e.g. disk failure). Also, the mock implementation can be much faster and thus allow you to run much more tests than the real implementation (e.g. an in-memory database).

The question you’re asking is answered right above in the quote. Idk what you’re trying to prove here. (If you’re trolling then well done)

2 Likes

No, not trolling; just “rusty” and confused.


Thank you! Until now, I have been under the impression that by “mock”, people were implying “dummy” or “void” implementations that were mere placeholders for the real implementation, so each real implementation could be tested in isolation - I had completely forgotten about the ability to simulate failures.

But a mock implementation is by definition is incomplete and therefore cannot provide a complete test - that can only happen with the real implementation (in the same way testing an electrical circuit with e.g. lightbulbs isn’t the same as connecting it to the actual system and e.g. it’s relays, actuators, pumps, etc).

Let’s suppose your mock implementations each simulate one mode of failure - if n effects are being used in the action being tested, then a thorough test will require 2n combinations of real and fallible mock implementations: the same as if the mock implementations were placeholders.

But now suppose, for some inexplicable reason, testing was required with all three sets of effect implementations - real, fallible and placeholder. Now a thorough test will require 3n combinations. That’s assuming, of course, each fallible mock implementation still only has one failure mode…

I leave it as an exercise to determine the number of required combinations if e.g. each fallible mock implementation has two or more failure modes!

Thank you. My curiousity feels satiated, and I think my opinion is also that we need to increase the usage of effect systems; people think they’re overly complex for the benefits they give, but they’re already very simple: both in concept and in code.

I’m not implying that you’re being untruthful, but I’m actually really curious about the effect system code you’ve dealt with professionally (would love an example snippet to understand it more), just to gauge how annoying effect systems could potentially be if they’re misused. I can kind of see effect systems causing a slight bit of indirection and hiding implementation details, but I don’t know how bad it would be.

Is there a collection of “compelling effect system examples” somewhere? That is, a collection of examples where the code written using a particular effect system (of extensible-effects heritage) is much clearer than the code written using MTL or ReaderT ... IO?

3 Likes

The effect semantics zoo springs to my mind, but I don’t think it’s exactly what you want.

1 Like

So it passes around the effect handler by passing the continuation to the function on the type system? Huh, that’s an awfully simple usage of delimited continuations… I wonder why some people are heavily against effect systems that use delimited continuations, then? I heard things like

if you have a way to make a -> {IO} b into a -> {}b, the latter means “yeah this function might do some impure stuff but it’s okay, you don’t have to provide a handler” and this is mind bending

or

delimited continuations make calling the wrong continuation or ignoring a continuation way too easy, which leads to your program being messed up too easily

(this is alll paraphrased from memory, i don’t particularly remember it)
since those complaints weren’t addressed here I assume they’re irrelevant? Or do they have some merit?

1 Like