If I have an Int, then I just have an Int.
If I have an Identity Int, then I really still just have an Int.
If I have a [Int] then I have a list of Ints.
If I have a Maybe Int, then I either have an Int or I have Nothing.
If I have a Reader r Int, then I have a computation taking some input of type r and producing an Int. That computation can't modify the value of r.
If I have a State s Int, I have a computation taking some initial state of type s and producing an Int. The value of s may change during the computation.
All of these are monads, but calling all of them "state" is somewhat reductive.
The Haskell wiki tutorial on monads refers to them vaguely as "strategies" - I mostly agree with that description.
I tend to think of a monad in computer science as something which is constructed from a type and a computation and wraps the return values of that computation in that type. That allows you to bake in "context" into that type which could be state in the conventional sense but it could be something else.
To make this practical, let's make a simple example of a simple somewhat useful monad with no state. Imagine you have all the regular trigonometric functions sine, cosine etc which accept radians[1] and you need versions which work in degrees. You could make a unit conversion monad which converts arguments on the way in and wraps the result in an inverse conversion monad such that if you passed it to arcsine arccosine etc on it it would know and return degrees on the way out.
Neither of those monads would have any state in the usual computer science sense, the main conversion is just multiplying all the inputs by pi/180 before calling the wrapped function and constructing a result monad type so the inverse trig functions do the opposite of that.
[1] ie the correct versions. Fight me physicists and engineers and you freaks who use gradians whoever you are[2].
[2] I think it's surveyors but I could be wrong.
The point of monads, from the point of view of programmer convenience, is that they let you have a bag of contextual stuff along with your values. Thus if you think of a server application that processes requests, you might want to carry all of:
- configuration applicable to this request
(or global configuration)
- request metadata (time received, from where,
from whom, authenticated how, etc.)
- request processing state
- logs/traces
- I/O methods, so that
- you can make all your code deterministic
and thus easier to write tests for
- so you can mock the world (see previous
item)
with each request. Thus each function that handles a request can get all of that context, and can be fully deterministic, yet it can function in a non-deterministic world.Besides this there's everything about the Maybe ("Optional") and Error/Either monads that just makes it easy to make sure all errors and "nulls" are handled.
And what makes all of this possible (in Haskell) is the use of operators (functions) and syntax that lets one write "statements" that are actually combined into large expressions.
you might want to carry all of:
- configuration applicable to this request (or global configuration) [...]
Ok, this is super-interesting to me, as I'm currently dealing with the "configuration and interfaces as global state" pattern in a legacy application. Among other things, this makes it hell to test and refactor. Refactoring it into a functional abstraction like this seems like exactly what we need.But ... I'm kind of struggling to figure out what "global configuration as a monad" would look like (very much not a Haskell programmer). How would something like this work in practice?
This is Haskell, but here's a really simple example.
-- As an ordinary function:
foo :: Int -> Bool -> Int
foo n shouldAdd = if shouldAdd then n + 1 else n - 1
-- Exactly the same, but using the Reader monad
foo :: Int -> Reader Bool Int
foo n = do
shouldAdd <- ask
return (if shouldAdd then n + 1 else n - 1)
Here's the same thing, but with a record with one boolean in it: -- Config: a record with one boolean in it,
-- and you access it with a function called `shouldAdd`
data Config = MkConfig { shouldAdd :: Bool }
foo :: Int -> Config -> Int
foo n cfg = if shouldAdd cfg then n + 1 else n - 1
foo :: Int -> Reader Config Int
foo n = do
mustAdd <- asks shouldAdd
return (if mustAdd then n + 1 else n - 1)
It's basically a way to have implicit read-only parameters. You can call a bunch of functions that take a configuration parameter without having to actually pass the configuration as a parameter explicitly. In an object oriented language, you might use classes for this (if not global variables).So everywhere you deal with a value that is of some type `Configured<T>` you can then refer to all the configuration methods you'd expect of you `Configuration` types. In Java you'd say something like `this.getConfigBlah()`, and it would just work.
> I'm taking some parameters and baking them into methods that I will call later.
Yes.
> But does this have anything to do with monads or some monadic version of state?
In languages that have monads: yes!
No, the whole point is that it’s completely dissimilar to that.
> But does this have anything to do with monads or some monadic version of state?
There is no monadic version of state. The State monad is a way of chaining things together (not unlike shell pipes) which is nice in Haskell because it gives access to some of the language syntactic sugar (the do notation). There is nothing particularly special there otherwise.
The key takeaway here is that you can avoid littering your global scope by explicitly passing what’s shared as functions arguments and then remove side effects if you also pass by value.
Yes, it is this simple.
I strongly, strongly suspect there's something about how F# processes monads in the context of a do expression that explains what is the focus here.
Unfortunately I don't have the time to go through the steps now, hence why I specifically recommended modifying the introduction to give some clue about the terms involved.
They absolutely are in the context of a program and not just abstract expression, which never matters. The distinction strikes me as meaningless. At the very least it's worth qualifying with "mutable state" if that's the attribute of state you care about. There's still "constant expressions" as other forms of values and "statically initialized values" as other forms of state.