Welcome @librarian!
I’ll even add one to your list: the State monad!
I think the short answer is they all do very similar things or potentially the same thing, but they just communicate intent slightly differently. Imagine how in C# you might have a class property with a get and set operation, or you might have a “getter” method and a “setter” method, which do the same thing, but communicate a little differently to the consumer about things like expected runtime and usage. Or in JavaScript/TypeScript how you can have a Map with string keys or an Object, which pretty much do the same thing, but communicate a little differently to the consumer about what kind of contents they might expect.
I think the different stateful monads can be summed up as follows
-
Stateis your go-to for most stateful computations, unless you have a reason to prefer some other monad. -
Readeris for (generally) read-only states, like application configs, where you set a value at the beginning and then don’t mutate it further. There’s thelocalfunction like you mentioned, which kind of lets you write to it, but you can’t return a modified value to the caller of aReader, so it still kind of preserves its read-only status. You could replace aReaderwithStateand it just wouldn’t be read-only anymore. -
Writeris for (generally) write-only states, like logs, where you consume a value only at the end but don’t inspect the value anywhere while running. You could replace aWriterwithStateand it just wouldn’t be write-only anymore. -
RWSis for situations where you have different things that are readable, or writable, or otherwise stateful all at the same time. So like if you have a config, a log, and some application state that changes as the program executes. It’s really just a helpful alias for having aReader, aWriter, and aStateall together as part of the same stack. You could actually replaceRWSwith threeStatemonads, or with a singleStateof a record that looked like
{ config :: r, logs :: w, appState :: s }. -
STis very similar toState, but with performance optimizations at the cost of added complexity (you have to understand and manage the “Region” of mutation). This is generally something you use as a performance optimization in some algorithm when mutating a value in a tight loop is significantly faster than “pure FP” approaches. You wouldn’t often useSTas just a general approach for managing some state, since it’s harder to use than just theStatemonad.
Hope that’s helpful! Please ask if there’s further questions or other ways to clear things up.