Monads are scary; or, The Queering of Object Orientated Programming
Published on 2022-04-25
I've been calling myself a functional programmer for a number of years, and I've worked on a number of projects in primarily functional programming languages. But still, there was always one thing that I could never figure out: monads. Today, that changed.
What is a monad?
In functional programming, a monad is a software design pattern with a structure that combines program fragments (functions) and wraps their return values in a type with additional computation. (Wikipedia)
While I was mindlessly scrolling through Wikipedia articles waiting for my computer science exam to start, I saw one article that linked to another called "Railway orientated programming," which I had never heard of. So, I clicked on it, and it redirected me to Wikipedia's page for monads. Gah! I thought. But then, the first line caught my eye, and it clicked.
Now, I say "that's it" as though I assume it'll be obvious to everyone, and it's certainly not. This is particularly the case if you haven't done much FP in the past. For me, it was the realization that I had been using monads all the time without even noticing. So, if that sentence doesn't make much sense, I'll walk through the example that same Wikipedia article gives. If you already know what monads are, you're not going to learn anything new by reading the next section, so feel free to skip it. Especially if you know enough about monads to poke holes in my explanation. We wouldn't want that to happen /s
Avoiding null pointer exceptions with
Having to code around null values is a pain, and they come up everywhere. If you're not constantly checking to see if a variable is null, then there's always that haunting chance that it'll show up where you least expect it. This can be a big problem in large code bases.
Many functional programming languages railroad you into making these checks by swapping out the classic "null" for a type similar to Haskell's
Maybe is a union type, and if your language doesn't have those, then all you need to know is that union types can be one of a finite set of things. Booleans are union types, and in some languages, that's how they're defined: either
Each "part" of a union type can also contain some value to go with it. That's what
Maybe, like the Boolean, has two options:
Just T or
T is some other type. Thus, if your function returns a
Maybe and you get
Nothing, then that means there's no result. Otherwise, you get
Just T, and if you properly unwrap it, you'll get the value you're looking for. This is that "extra computation" the Wikipedia definition was talking about.
So that's an example of a monadic type. Monadic types are just one part of the larger pattern. A monad also needs two functions: one that wraps a type in the monadic type (say, taking
t and turning it into
Just t) and another that takes a value wrapped in a monadic type and turns it into another value wrapped in a monadic type. You can think of that as two functions "passing" the value between each other, without having to go through the process of unwrapping the
Maybe every time. With those three simple things, you can accomplish quite a bit that would otherwise seem impossible in functional programming.
Why is it so hard?
I've spent most of my life thinking that monads were an extremely abstract mathematical concept that just kind of happened to sneak into programming languages, and that they were only supposed to be understood by grad students doing their Master's thesis on category theory. I've also spent my entire life being told by functional programmers that monads aren't some abstract, esoteric concept, and that they were pretty intuitive given the right framework. Of course, those functional programmers were never able to transfer that framework to me. Whenever they'd try, my brain would just shut them out.
I never really thought of myself as an "object-orientated programmer" (at least, not until I started having to fill out job applications that insisted on applicants being well-versed in OOP, for whatever reason). To borrow a concept from gender studies, "object-orientated" is a sort of hidden adjective when talking about programming. Hidden adjectives are the adjectives we convey implicitly--not because they are necessarily true, but rather because they're understood to be the norm1. Not every programmer is necessarily an object-orientated programmer, but it feels kind of weird to make that explicit considering how common object-orientated programming is. Notably, it doesn't feel nearly as weird to call someone a "functional programmer;" that's less obvious and as such needs to be marked.
There's a certain ideology present among a lot of computer science educators that OOP is the natural way to write code. There's nothing natural about this, and in fact, not too long ago, OOP didn't even exist. Object-orientated programming certainly feels pretty natural when you're introduced to it through the textbook examples that ensure that every object pairs perfectly with some real-world, unabstracted "thing," like
BlueCar. However, those models poorly represent the kind of abstractions you need to make when programming.
I can remember sitting in my first-year computer science class where, for the first time, I got the actual, textbook definitions of polymorphism, inheritance and encapsulation. Seeing those examples we covered in class worked their magic on me as well, and coming out of it, I was feeling a lot more inclined to lean on classes in my own projects. However, I've also seen the nightmare that your code base can turn into when you're not careful with it. And it's kind of hard to talk about that with people who've been freshly indoctrinated into object-orientated ideology--at least, not without being a weirdo.
Side note on "getting good"
There is a reasonable argument that could be made by senior software developers who work primarily in OOP that I'm just bad at it and don't know what I'm talking about. I agree with both of those conclusions (without being too hard on myself). There are libraries worth of books written on how to do OOP correctly that promise, should you follow in their footsteps, that you can avoid all of those "nightmares" OOP "causes."
I do, however, want to note that what I'm talking about here has more to do with creating a system in which newer programmers can do more good things quicker. The same kind of argument can be applied to the case of C++ versus Rust. Sure, you could just get good at memory management and accomplish everything that Rust can do with a much more mature technology. Or, you could just let everything explode and patiently walk through your problems with the compiler. The latter (reasonably) guarantees that things won't go wrong.
A very productive conversation
I do have one friend though, going to a different university, who was very interested in the concept. She was taking a very different first-year computer science course that taught Scheme and C instead of Java. So, her first exposure to programming was functional. As someone who was interested in continuing with the program, this person was interested in what to expect from the upper-year computer science courses that focused on OOP.
But, how do you explain OOP to someone who's only ever used functional and procedural programming languages?
What is object-orientated programming?
It's a different way of framing your code. Instead of doing everything with functions, you're trying to do everything with classes.
What's a class?
A class is kind of like a container for data.
So like a list?
Kind of, but things aren't indexed by numbers. Every element has a name.
So like a struct?
Not exactly. The thing that makes classes unique is that they have methods that are bound to the data.
What's a method?
Well, a method is kind of like a function.
What's the difference?
A method can only act on the instance of the class.
So it's like a less useful function.
Well, it can be just as useful if your functions are polymorphic.
A polymorphic function is a function that can have more than one type of value passed to it.
So like a generic function?
If you keep trying to explain this, as I have, and you're about as biased towards functional programming as I am, you'll start to feel like you're reinventing everything that can be done with procedural or functional code in a less obvious and more complicated way.
Now, the takeaway from this isn't that functional programming is somehow superior to object orientated programming. In my experience, it feels a lot more like a matter of preference. It's just interesting to consider how our "upbringing" in programming shapes the way we think about everything we subsequently encounter. When you're trying to transition to a primarily functional programming language, it's really hard to unlearn all of the things that have been drilled into your mind by your CS professors and/or boot camp instructors. The biggest thing that turned me away from Haskell when I first approached it was that I simply couldn't imagine how I would create any program more complicated than a calculator with it. It just seemed unimaginable to me.
The same thing goes for everyone who was first introduced to programming in my friend's Scheme class.
Technically, the class I took in my first year first introduced us to procedural programming before moving into object orientated programming in the second semester. I still take issue with the fact that OOP was treated as somehow being the natural evolution from procedural, which, as far as I'm concerned, is completely untrue. Maybe I'm just miffed that my school doesn't offer any courses that cover functional programming. I don't even take issue with the fact that object orientated programming is so common in universities. This makes a lot of since considering how popular OOP is in the industry. How could they not teach it? What matters to me is how swift people are to naturalize it. Instead, if educators focused on introducing both OOP and FP, students would come away with two unique tool sets to solve unique problems in elegant ways.
Knowing more than one way to solve a problem goes a long ways towards enabling you to synthesize your own solutions to new problems that you never could have anticipated. That's a pretty general framework for solving problems in any discipline. To do that, one must grow out of the frameworks they've always thought natural.
- Object-Orientated Programming is Bad by Brian Will. This video inspired some of the ideas from this post.
- If the instance is down, copy this to your preferred Invidious instance:
- If the instance is down, copy this to your preferred Invidious instance:
A. Braithwaite and C. M. Orr, “Knowledges,” in Everyday Women's and Gender Studies, 1s ed., New York: Routledge, Taylor & Francis Group, 2017.