Monads are state/system backdoors for pure functions. Life would be pretty cool if we could just map/filter/reduce everything, it's a beautiful model and ripe for insane optimization. Sadly programs like that aren't useful, and programmers always want to do gross animal stuff like writing to sockets or files. Enter the humble monad, a way to pass something like a database connection, a logger, a system IO instance, or whatever into your cool nest of map calls.
John Ousterhout talks about this problem in A Philosophy of Software Design. Sometimes you need to carry things around, like a rendering context, a browser window, whatever, and you're give a couple of imperfect options:
- Make it global
- Thread it through literally everything
(React people might know #2 as "prop drilling", and they might know #1 as hooks)
Monads are infrastructure to help you do #2, since #1, well, people don't like it.
Option 3: Create an object that carries it around. (which, if you squint, is exactly what the monad is - an object that carries the data around. The difference is that monads always have the same interface, no matter what extra data they carry around. I'll let you decide for yourself whether that's a plus or a minus...)
I see where you're coming from, but I wouldn't say it's different than "thread it through everything". The monad way is to pass a blob through a pipeline and use types to say "this thing I'm threading through the pipes is an integer, but also a logger". The convenience here is that the extra state is attached to your data, so you don't have to explicitly thread it through each function. But for that convenience, you're (almost always) using up memory or suffering dereferencing (or both).
This dichotomy is fundamental, which is why I'm sticking on it here. Either you have to deliberately hand this piece of data down into the scope you're working with, or you can summon it from any scope. There really are no other options.
But doesn't that mean that you're agreeing with me that OO and monads are doing the same thing (at least in this regard)?
And, can someone explain to me how the type system works here? I had thought that an advantage of monads were that you just had to change the type signature to take a monad rather than to take an int, but once you did that, it could take any monad. But that won't work, will it? If you're going to try to log from that function, then it has to be a monad that logs, doesn't it? It can't just be a generic monad; it has to be a specific one.
Re: type system, AFAIK you do like (let's assume something Go-ish here):
type Integer interface {
GetValue() int
Mul(i Integer) Integer
}
type Logger interface {
LogMessage()
}
type DataBlob struct {
value int
message string
}
func (db *DataBlob) GetValue() int {
return db.value
}
func (db *DataBlob) Mul(i Integer) Integer {
db.value *= i.GetValue()
return db
}
func (db *DataBlob) LogMessage() {
log.Println(db.message)
}
func triple(db *DataBlob) *DataBlob {
return db.Mul(&DataBlob{3, ""})
}
---
In this way, you can always implement more interfaces onto DataBlob, and then modify your pipeline steps to take advantage of the new functionality. This example isn't really perfect, but it's reasonably illustrative. The thing is getting the compiler to know you can do different things with a given piece of data. In Go that's interfaces, in Haskell that's typeclasses, in Rust that's traits, blah.
P.S. This [0] Rosetta Code example made it pretty clear to me, so if you're the same kind of thinker maybe it'll help you.
i'm with the gp. i guess another way i think about it (which might be wrong, please tell me) is that with monads we can talk treat something like pipe as a pure object, even though whats its carrying is decidely not
Yup, exactly. It's basically like if you have a bunch of simple arithmetic functions:
half(x)
double(x)
triple(x)
you can easily do things like:
double(half(triple(13)))
Or a la pipes:
triple 13 | half | double
But now what if you want to print something in `half` without a global? Well, I guess it's half(x, printer) now. Hmm and now `triple` has to carry that around too, so it's triple(x, printer). Well, that makes me think everything gets a printer, so it's double(x, printer) now too.
Well, printing is cool, but now we're a real company and real companies log everything. I... guess we're making new functions? half_log(x, logger) and triple_log(x, logger) and double_log(x, logger). And... new pipelines? Ugh.
At this point you might be thinking one of a number of things:
- Are globals really that bad?
- Can OOP fix this (dependency injection, builders)?
- Surely there's some kind of framework for this.
And if you're a functional programmer who's super not into globals or OOP, you might start seeing the appeal of #3 there. Et voila, a whole bunch of functions that handle this "pass an extra blob into your pipelines" thing.
Monads are state/system backdoors for pure functions. Life would be pretty cool if we could just map/filter/reduce everything, it's a beautiful model and ripe for insane optimization. Sadly programs like that aren't useful, and programmers always want to do gross animal stuff like writing to sockets or files. Enter the humble monad, a way to pass something like a database connection, a logger, a system IO instance, or whatever into your cool nest of map calls.
John Ousterhout talks about this problem in A Philosophy of Software Design. Sometimes you need to carry things around, like a rendering context, a browser window, whatever, and you're give a couple of imperfect options:
- Make it global
- Thread it through literally everything
(React people might know #2 as "prop drilling", and they might know #1 as hooks)
Monads are infrastructure to help you do #2, since #1, well, people don't like it.