What should I use for effect handling?

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

That doesn’t make sense. No effect system directly gives you “transactional state”, not even StateT s (Either e). In order to run it on a state you have to runStateT the whole thing, at which point you get a s -> Either e (s, a) and you’re no longer in a (stateful) effect system anyway.

Now, you could say that

transactionally :: s -> StateT (Either e) a -> StateT m (Either e a)

is an implementation of transactions in MTL. But then

transactionally :: s -> Eff (State s : Error e : es) a -> Eff (State s : es) (Either e a)

in an implementation of transactions in effectful. That is, transactionality is not a property of the effect system, it’s just a combinator you write on top of your effect system.

3 Likes

Thanks for persevering with my queries. I’m glad it’s not beneath you to respond to relative newcomers like myself.

Well Either is deficient here isn’t it? In the sense that it does not carry the information we need at the points we need it. So I suppose modelling with a “richer” type might be a way to fix things in StateT. But personally I don’t like the MonadBaseControl way of doing things as you described it, sounds like a bit of a hack.

I’m happy if I can be of some help!

Well Either is deficient here isn’t it? In the sense that it does not carry the information we need at the points we need it. So I suppose modelling with a “richer” type might be a way to fix things in StateT.

If Eitheris deficient then it’s not a deficiency of only Either. As I described above, to “fix” the problem, whilst remaining in the “arbitrarily composable monad trasformers” space requires a change to not only Either/ExceptT (or whatever else represents the failure effect) to allow it to carry an arbitrary state in its failure condition, but also a change to StateT (or whatever else wraps the failure effect) to preserve its state through the failure case. On top of that you need a common convention like MonadBaseControl that allows them to work together in a way that preserves that state.

Once you’ve done that you have strongly restricted the class of monad transformers that are composable with each other, and you’ve lost the benefit that monad transformers were purported to have in the first place. So I would describe it a deficiency of the “arbitrarily composable monad transformers paradigm”, not as a deficiency of Either.

You would attain different semantics by explicitly using a different effect (that includes a ‘transactionally’ operation as @tomjaguarpaw said) and handler in this case, and I think this is better than waiting for different semantics to fall out of arbitrary orderings of handler calls. To quote Alexis King:

I believe there is a ground truth that exists external to monads, transformers, and continuations. I believe an effect system should pursue it, acknowledging the advantages of adopting a model without confusing the model’s limitations for features of the terrain. Both semantics for Error composed with State are useful, and we should be able to express them both, but we should do this intentionally, not incidentally.

1 Like
action :: Effect1 -> Effect2 -> ...
action e1 e2 = ...

action' :: Effect2 -> Effect1 -> ...
action' e2 e1 = action e1 e2
1 Like

What are you trying to say?

For future reference, efficient communication usually involves straightforward descriptions of problems at hand and potential ways of solving them, rather than vague sarcasms, references, or allusions.

5 Likes
2 Likes