What should I use for effect handling?

Why’s that a problem? If it’s not used, then just don’t use it!

Perhaps you could point to some example code, because I’m finding it hard to understand in the abstract what exactly you are looking for.

I mean if I were to use each as a separate parameter, I would have something like:

foo :: A -> B -> C -> D -> E -> IO ..
bar :: A -> C -> D -> E -> IO ..
baz :: B -> C -> D -> E -> IO ..
barf :: A -> B -> C -> E -> IO ..
baze :: A -> B -> C -> IO ..

Grouping parameters do not make it more convenient here.

Why not? Why not put them all in

Params = Params A B C D E

?

@tomjaguarpaw I don’t understand why you’re so adamant on using this (suboptimal) technique.

We had similar conversation on reddit recently and I pointed out the same thing @Abab9579 says, i.e. that passing these explicitly is unwieldy for more than a few effects and if you want to group them in a record, you’ll have to create a bunch of records because your functions take various subsets of these.

This will work for foo. What about the rest of functions?

1 Like

I don’t understand why you’re so adamant on using this (suboptimal) technique.

I don’t understand why you think it’s suboptimal! Normally in functional programming when we want to depend on something we pass it in. Why not for depending on effects, just the same as for depending on normal values? And if you get really tired of passing it in manually, use a ReaderT.

This will work for foo. What about the rest of functions?

The rest of the functions also take a subset of those parameters, so will work equally well. This is no worse than two other situations @Abab9579 might be in:

  1. Already using a sequence of concrete monad transformers, so passing in each one individually is not really worse

    AT a (BT b (CT c (DT d (ET e m)))) a
    

    is not really better than

    A -> B -> C -> D -> E -> m a
    
  2. Already using a single concrete monad

    AppT m a
    

    is not really better than

    ReaderT Handles m a
    
2 Likes

It won’t, because now your type signatures “lie” in a sense that it looks like they use all these effects, whereas in reality they don’t.

This is your post from the other thread:

With what you propose you lose this propoerty, don’t you? :thinking: Now it looks like your functions are using more effects than they do in reality.

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

2 Likes

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?)