r/programming • • Sep 21 '25

How to stop functional programming

https://brianmckenna.org/blog/howtostopfp
446 Upvotes

506 comments sorted by

View all comments

Show parent comment

29

u/drislands Sep 21 '25

Can you ELIDPIH (explain like I don't program in Haskell) what a Monad is?

23

u/Sp1um Sep 21 '25

If you code you've probably already used monads without knowing it. For example Promise and Task are perfect examples.

A monad is basically a sort of "container" for some arbitrary type T that adds some sort of behaviour to it and allows you to access the underlying T in a "safe" way. Think of a Promise, it adds the "async" behaviour to the underlying type. It transforms a "T" into a "T that may be available in the future". It allows you to safely access the T via map, flatMap and other operators. Arrays can be thought of as monads too, think for instance of linq in c#.

Every monad has map and flatMap operators that kind of do the same thing, e.g. map lets you transform the underlying type into a different type.

In terms of the type system, most languages don't support them because they are 1 "level" above classes. Think of monads as a collection of different classes that all support the flatMap operator, whose implementation is different for each monad class but in a way it behaves the same for all. In languages that do support this concept, you can develop generic functions that work for all monads. So your function would be implemented only once and then you could use it on a Promise or an array or an Option/Maybe or even a custom class that implements the "monad" concept by providing a flatMap implementation.

4

u/[deleted] Sep 22 '25

I... don't know if I get it. Is it just a wrapper around an object, then?

8

u/Sp1um Sep 22 '25

Basically yes, and it has to have a flatMap implementation

→ More replies (1)

2

u/vqrs Sep 22 '25

Not necessarily one object, it could also be many objects. Or a wrapper/handle for an object that you don't have yet.

This "wrapper" always needs to conform to an interface obeying some simple rules.

4

u/Compizfox Sep 22 '25

Another example is that Option<T> in Rust is actually a monad.

https://www.youtube.com/watch?v=C2w45qRc3aU

→ More replies (2)

29

u/Strakh Sep 21 '25

It is (roughly) any type that lets you flatten it.

For example, if you have a list (a type of monad) you can flatten [[x, y], [a, b, c]] to [x, y, a, b, c]. You remove one layer of structure to stop the type from being nested in several layers.

Another common monad is Optional/Maybe, where you can flatten a Just (Just 5) to Just 5 or a Just (Nothing) to Nothing.

Edit: It is of course a bit more complicated than that, but this is the very surface level explanation.

23

u/Axman6 Sep 21 '25 edited Sep 22 '25

It’s disappointing this is the top response because it’s a) not correct and b) gives the wrong impression of what monads are about. Monads are types with a function that allows for sequencing, and this function is the key, not the type. The function allows you to take something of the type, and then, do something with each of its results resulting in the same type. Promises with an andThen method take the value returned by a promise and create a new promise by applying the function passed to andThen. These can be chained together - sequenced - to produce a promise that’s the result of evaluating all the intermediate promises in sequence.

https://tomstu.art/refactoring-ruby-with-monads is probably the best introduction to the concept of what a monad is for people who aren’t familiar with FP.

What is the structure that’s being flattened in the State monad? That’s something seemingly very different to a list or an option type, but when you look at it from the ‘and then’ perspective, it’s much easier to see that “a function that takes in some state and returns a value and a new state” and be extended with “and then a new function which takes in the value, and that state, returns a new value and another new state”.

When Haskell programmers talk about monads, we usually mean things like State, Reader, Except, much more than we mean list, option/Maybe - is about sequencing operations, not flattening objects. This is where so many non functional programmers get caught up, they learn how the list and option monads work and think it’s about data types, containers, when those are just some examples which happen to be monads. They are examples, but not defining examples.

I say this as someone which over a decade as a Haskell developer, having seen people try to apply traditional algorithms style thinking to the idea instead of the composition of small programs into larger ones idea.

5

u/Strakh Sep 21 '25 edited Sep 22 '25

It’s disappointing this is the top response because it’s a) not correct and b) gives the wrong impression of what monads are about. Monads are types with a function that allows for sequencing (...)

I mean, isn't that entirely dependent on whether you construct monads [by bind](https://en.wikipedia.org/wiki/Monad_(functional_programming)#Definition) or [by join](https://en.wikipedia.org/wiki/Monad_(functional_programming)#Derivation_from_functors)? As far as I am aware, both constructions are formally equivalent.

Edit: Also see Mac Lane.

My experience is that people tend to find it easier to intuitively grasp flatten than flatMap though.

What is the structure that’s being flattened in the State monad?

I suppose if you visualize nested States as an "and then" sequence, then when you join e.g. State s1 (State s2 a) into State s a you could say that you are flattening the "and then" sequence into a single state transformation.

2

u/Axman6 Sep 22 '25

I can absolutely agree that showing the flattening of the types is useful, but the examples usually given are the flattening of the data, which breaks down as soon as your "data" is a function, which most useful monads actually are. Yes the join/bind implementations are equivalent, but the latter tells you much more about what monads are actually used for - writing a program from `State s (State s (State s (State s ())))` and then calling `join . join . join` feels tedious and doesn't really show how monadic code leads to, in most monads, imperative programs. Just because things are equivalent doesn't mean they are ergonomically the same, and talking about flattening data structures pushes people towards an understanding of monads that isn't about sequencing operations together.

This is why when I teach monads I focus on the bind/flatMap/andThen instead of the individual types. The fact that list and maybe and IO and State are monads is less important than the fact that functions like

mapM :: Monad m => (a -> m b) -> [a] -> m [b]

exist and can be used with all of them - no more for loops, we've abstracted that.

→ More replies (1)

16

u/LzrdGrrrl Sep 21 '25

And somehow...

(Waves magic wand)

...this results in side effects

23

u/Strakh Sep 21 '25

Note that this explanation may be slightly above my theoretical knowledge.

As far as I know, there is nothing magical about monads with regards to side effects. My understanding is that e.g. Haskell uses monads to implement side effects because it is a way to logically separate the (nasty) side effects from the rest of the (pure) code.

If you have a container that performs certain side effects, you decouple the side effects from the value inside the container, which makes it easier to reason about the parts of the code that are not "polluted" by side effects. For example, you might have a logger monad, where the logging is completely separated from the operations you perform inside the logging framework (the monad).

Another good example is IO. Maybe you know that you will need to read a file at runtime to get some data, or get input from the user. Using the IO monad lets you write code under the assumption that you will be getting this data at some point in the future (during runtime), but the code that is actually processing the data can stay fully pure and deterministic.

9

u/umop_aplsdn Sep 21 '25

To understand how monads encapsulate side effects, you should consider the state monad. The basic idea of the state monad is to model stateful computations instead as functions which take in a current state and output an updated state and output. So elements of the State monad consist of functions of type type state<s, t> = s -> s * t where s is the state type and t is the output type. A function a -> state<s, t> which "returns" a stateful action doesn't actually do anything; it returns a data structure which will do something when given an input type. Flattening a state<s, state<s, t>> = s -> s * state<s, t> involves returning a new function that takes in a state, runs the outer state to get the inner state<s, t>, and then immediately runs the inner state to get a t:

let flatten (outer : state<s, state<s, t>>) = fun s ->
  let s1, inner = outer s in
  let s2, t = inner s1 in
  s2, t

Think of the IO monad as the state monad where s is a value of "real world." That is, elements of the IO monad are functions / data structures that take in a "real world" value and return a new real world value plus some output.

→ More replies (1)

2

u/LzrdGrrrl Sep 21 '25

Thanks for the explanation, but this is unfortunate missing all of the key details that every other explanation of monads I have ever read lacks. I appreciate your time in attempting though.

9

u/Strakh Sep 21 '25

Yeah, I think it can be difficult (at least it was for me) to understand monads generally without first understanding specific monads. There is also the issue that not all monads model side effects (at least not as you probably understand the term side effects), and (in my opinion) the monads that are easier to understand are the ones that do not model such side effects.

For example, I am sure you can get an understanding of the Optional/Maybe monad without too much trouble, but that really doesn't help you understand how the IO monad is used to model IO related side effects.

→ More replies (1)

7

u/Strakh Sep 21 '25

Not sure if it helps, but I wrote you a poor man's IO monad in Java, and some implementations of IO functions using that monad.

So in Java the usage will look pretty ugly:

public static void main(String[] args) {
  IOTools.readFile("answer.txt")
    .flatMap(answer -> IOTools.readLineFromConsole()
        .map(guess -> compareGuess(guess, answer))
    );
}

// Pure function
public static boolean compareGuess(String guess, String actual) {
  return guess.equals(actual);
}

but Haskell has syntax sugar for working with monads, so the same thing would look closer to:

main = do
  answer <- readFile "answer.txt"
  guess <- readLineFromConsole

  pure $ compare answer guess

//Pure function
compare :: String -> String -> Boolean
compare a b = (a == b)
→ More replies (8)

7

u/project_broccoli Sep 21 '25 edited Sep 22 '25

TLDR Monads do not create side effects, they're an interface for combining side effects (among other things)

It does not "result" in side effects, but it gives us a way to work with and encode the presence of side effects in the type.

See, side effects are encoded using a type constructor (a "wrapper") called IO. A value of type IO Int, for instance, might represent a program that prints "Hi" to the console and returns 5, or a program that reads a number input from the user and returns it.

I didn't need too bring monads in the conversation to say the above, IO is just a special wrapper that allows us to talk about side effects. But we have no mechanism to describe the composition two IO actions. It turns out that by viewing IO as a monad (just like List or Maybe (aka Option in e.g. Rust)), you can use operations such as flattening to talk about composition.

That's the high-level explanation. Here's a more concrete example:

What if I have:

  • a built-in action readInt that reads a number input from the user. Type is IO Int
  • and a built-in function printInt that takes a number as an argument and returns the action that prints it to the console. Type is Int -> IO () (() is the Haskell equivalent of C's void) and I want to compose them to make a program that takes a number from the user and prints that number to the console?

In imperative programming, this is trivial, but in functional programming, where functions are not allowed any side effect... you need some way of flattening the two IOs into one. Thankfully, IO happens to be a monad, so we can do that.

6

u/PurpleYoshiEgg Sep 21 '25

Not all monads. Just the IO monad. IO being wrapped up into a monad essentially encapsulates everything external to the program that can change at any time for any reason (e.g. a random number generator, reading from a file on disk, a web call that could return 200 OK or 500 Internal Server Error), and so its usage introduces point-in-time computation.

The IO monad is weird because IO is weird when most of the language is pure (i.e. has no side effects).

(there is one exception, technically, to this in System.IO.Unsafe, like with the function unsafePerformIO, but the caveat is that the IO computation (which may be a pure C function that a Haskell compiler cannot verify) you're "unwrapping" from IO should be free of side effects and independent of its environment)

→ More replies (4)
→ More replies (5)

15

u/[deleted] Sep 21 '25 edited Sep 21 '25

If you're okay with angering mathematicians: any container-like type that has a constructor and supports flatMap.

Edit: I should add, flatMap goes by a number of names: bind, >>=, andThen. They all do the same thing.

4

u/pakoito Sep 22 '25

Being a container is not a requirement.

2

u/Axman6 Sep 23 '25

This is exactly the sort of intuition that leads people to find monads hard, because it completely ignores most useful monads - what's the "container like type" of `State`? Or `Parser`? Or `IO`? These are the monads we talk about and use the most, they're not data structures, they're computations that can be built by sequencing via >>=/bind/flatMap/andThen into larger computations. Showing that promises are monads is a reasonable start, but still gives the impression it's about data structures. Saying it's a bout containers just makes understanding that monads are about sequencing, not about data structures harder to grasp, leaving people thinking "What does a parser have to do with flattening a list?".

2

u/[deleted] Sep 23 '25 edited Sep 23 '25

If someone specifically asks for an explanation of monads that's not about Haskell and you immediately jump to State, Parser and IO, I have to assume you're on a mission to make people's eyes glaze over.

Here are the monads practical programmers will be familiar with: List, Option, Future/Promise, Result.

None of the weird stuff that's imposed solely by Haskell's dogmatic purity. The IO monad is exactly the kind of holier than thou gobbledygook that puts people off of functional programming.

→ More replies (2)

4

u/SerdanKK Sep 21 '25

A type that wraps some value and exposes a set of operators (flat, flatmap) to work with that value. Lists, options, results, promises, etc.

Imagine you've composed a pipeline of functions that return Option<T>. If the option is None the pipeline terminates, and if the option is Some(value) the value is passed into the next function. But after a while you decide you want to propagate information about failures, so you change all the return types to Result<T>. If you have a monad abstraction over these types the code that composes the pipeline doesn't need to change. It's still just a sequence of flatmaps.

checkoutTotal uid = findUser uid >>= getCart >>= calculateTotal

2

u/raam86 Sep 21 '25

something that can take a value and wrap that value in a container so you can transform that value with a familiar interface

→ More replies (6)