What should I use for effect handling?

Sure, but only in the sense that pure () :: MonadState Int m => m () is a lie. It’s not a very bad lie.

With what you propose you lose this propoerty, don’t you? :thinking:

Yes, it’s a lie that acts against that nice property.

Looks like he’s using a polymorphic monad and class constraints

Yes maybe. I’m having trouble understanding the baseline for comparison, which is why I’m having trouble making a concrete suggestion. Most of my dialogue in this thread has been Socratic.

That said, I’ve been doing some experiments with “handles as arguments” and so far I’m very pleased with the experience, versus “handles only in a type-level list”.

Oh I forgot, there are also things like

createC :: A -> B -> IO C
createE :: A -> B  -> C -> D -> IO E

in addition to the above, used to create other handles.

For instance, Logger is positioned at the baseline and used for constructing other handles.

This is also why I found “tying the knot” approach enticing. I am wary of the additional dependency footprint though.

Interesting! I think I’m finding this code hard to imagine. I can certainly imagine a logger effect being used in handling another effect, but I can’t imagine it being used to create another effect. I guess at this point I’m out of suggestions unless you can link to some real code.

Sorry that I cannot specify concrete code now.
I could roughly talk why I need logging for creating handles though.

For instance, I find it good practice to log when connecting to a DB. That way, I can know if error happened later, and what was the problem.

I am making a desktop application for linux, and interfacing with DBus.
Connecting to DBus could be complicated, so I also put some logging inside it.

2 Likes

Oh yes, makes perfect sense. I guess I am just too used to seeing handlers in CPS form, such as

withC :: A -> B -> (C -> r) -> r

So I rescind my previous puzzlement.

2 Likes

Look at e.g. section 4.2.1 (pages 32-33 of 59) of the associated article.

Thank you, I should stop caring about having too many parameters and embrace the length of types.
With some global variables sprinkled around, it should be manageable I think.

2 Likes

createC :: A → B → IO C

I find it good practice to log when connecting to a DB

fwiw, it tried to tackle this requirement with the “build a fixpoint” approach. I did not find a direct way. The closest I managed to do (gist) was to return “initializer actions” along with each component, accumulate them, and execute them before application start. But it complicates the component somewhat, and allows components to exist in an unitialized state.

Perhaps libraries like registry could handle this is a more natural way.

2 Likes

I am new to these effect handling libraries, pardon my ignorance. I see one of the motivations for effectful is the behaviour of StateT in the event of an exception. I am referring to this section of the effectful README:

The problem is that state updates tracked by StateT within a computation wrapped in catch are discarded when an exception is raised. This is confusing and will lead to bugs if one doesn’t know about this subtle behavior.

Isn’t this just a matter of working together to ‘correct’ this behaviour in StateT code? If indeed there is some consensus it actually agree it requires fixing I suppose.

Around the section below the quote above, regarding the WriterT space leaks. If people are adopting CPS.WriterT, then WriterT being broken is not so relevant anymore is it not? At least not relevant as a motivator for the effectful library.

For references, here are two very helpful documents from the Effectful repository by Andrzej that each outline reasons why the status quo must change:

1 Like

I was responding to some of the content in your first link.

No, it can’t be corrected in StateT, by the nature of StateT itself. StateT s m by definition keeps track of state by returning it in the result of each action in m. If some action in m has no result (such as throwIO :: IO a, or throwError :: ErrorT e m a) then that state is lost forever. There’s no way to get it back.

So, if you want to be able to reference the state after such an action the state needs to be stored in a reference that is accessible regardless of the action in m, such as an IORef, STRef, or implicitly such as in the way that effectful handles state.

You could also just keep State on the “outside” of your other effects:

ghci> runState (runExceptT (lift (put False) *> throwE "error")) True
(Left "error",False)

In fact, handling state through IO is very similar in this regard, because IO also always needs to be on the “outside”.

2 Likes

Sure, but that doesn’t work for exceptions in IO, which must be the base monad (it doesn’t have a transformer form) or pure exceptions (error, undefined, throw) run in any base monad.

(Terminological question: don’t we usually use the terminology OuterT InnerM a, so in this case you are actually suggesting keeping state inside the other effects?)

I do not understand the rationale for StateT with ExceptT. Ofc we should not use them with IO. Yet I do not see the problem in pure transformer stack, indeed order affects semantics! That is the whole point.

So if you don’t want to use IO, nor recover from exceptions thrown from pure code (error, undefined, throw), then you can use MTL/transformers, and the benefit you get in return is that you can combine them in different orders to (sometimes) get different semantics. That much I agree with.

Why is it the whole point? A property “rearranging the order doesn’t change behavior” is very nice to have, not to mention that it decouples the notion of “I want the shape of returned value to look like this” from “I want the code to behave like this”. There’s no reason to expect these should be related.

My opinion is that StateT/ExceptT situation is an accidental misfeature stemming from their definitions (similar to StateT losing state updates on exceptions, which some people try to defend talking about “transactional behavior”) that bites you sooner or later. YMMV though.

2 Likes

Thanks. Could this not be more accurately described as a deficiency of the monadic type m rather than a problem inherent in StateT, in that as you allude to an error cannot be represented in m and hence returned as part of a runStateT ?

Could this not be more accurately described as a deficiency of the monadic type m

Perhaps, if you consider Either e (as a Monad) to be “deficient”. By design, Either e a contains no as in the Left case. That’s it’s whole point! And StateT s (Either e) keeps track of its state by returning it as part of the a. So when there is no a (which is desirable) there is no state.

To cope with this you can either may Either e and StateT s cooperate, by arranging for StateT s to smuggle its state into the Left case, to be restored later (that’s the MonadBaseControl way), or you can just embrace one monad to rule them all (that’s the ReaderT ... IO and effectful way).

The MonadBaseControl way proved too complicated in practice, and it didn’t really work properly anyway. The ReaderT ... IO way doesn’t allow you to remove effects and return to the pure world. The effectful way seems to be the best of all worlds.

2 Likes

Hrm:

  • Bob wants the current state to be preserved across errors as much as possible in his program;

  • Jan wants transactional behaviour - errors discard the current state - in her library;

  • They both then try to use effectful.

…can effectful “read minds”?

1 Like