When I started my first full-time job as a software engineer out of college, I frequently had discussions with my fellow new-hires about our technical adventures up to that point. At one point, two of them mentioned something called a “monad” and went back and forth about it for a few minutes. My first thought was:
“Monad?? What is a monad? Am I supposed to know what that is?!”
I remember just nodding along as I had this conversation in my head. The first thing I did when I got back to my desk was search “What is a monad”, as any curious programmer would. I was greeted with a plethora of articles, all of which made me scratch my head after reading. I had to get back to work but I decided to bookmark a few more articles and continue later. Safe to say the proverbial can of worms was opened.
What is a Functional language?
A very small number of companies actually use functional languages in a professional capacity, so how could learning about them help me? To answer that I think it would help to agree on some of the central tenants of functional programming.
- One of the core principles of functional languages that stuck with me right away is that you cannot mutate data once it has been created. Instead of directly mutating the data like you would in an imperative language like C++, you just make a fresh copy with the desired update. This is also referred to as stateless programming. One benefit of stateless programming is that you do not have to worry about multiple threads modifying the same data because each thread gets its own copy.
- Referential transparency essentially means that you can replace an expression with its result. So instead of
1+2you could simply write
3. That’s a trivial example, but where it becomes more interesting is with functions. With referential transparency you don’t have to worry about the function producing side effects or mutating some global state that affects other parts of the program. You can legitimately replace the function call (an expression) with its result and be done. Functions are also repeatable which means they don’t depend on external state themselves. Some other phrases thrown around when describing functional programming languages are “pure” and “no side effects”, and I think referential transparency encapsulates both of those. This contrasts with imperative languages because if I call a function
foo()in C++, I have no way of knowing if it updated some global state! It’s not always true that I can replace
foo()with it’s return value and expect the program to behave the same way.
- Another major characteristic of functional programming is the fact that functions are first order citizens. Many object-oriented languages have begun to add this to their feature set, but higher-order functions are rooted in functional programming. This means you can do things like pass functions into other functions, receive new functions as the result of a different function call, and create anonymous functions (also known as closures). When we create a closure, we create a function on the spot and capture some data that we’d like to use in the function. This is useful because we can then pass that closure (which is just a function) into other functions and it will always have the data it originally captured to perform its task. For example, these constructs allow a language to implement the
mapfunction, which applies a function
fto each element in a collection - you can’t do that without sending
fin as an argument to
map. In this case
fcould be a function we’ve already defined or it could be a closure we just created with some extra data. This provides an incredible amount of flexibility that I’m only beginning to understand how to wield!
This is not an exhaustive list, these are just the main concepts I think about when it comes to functional programming. The more I program in functional languages, the more I am learning about the implications of each of these principles as well as other core tenants of functional programming. Later in this post I’ll highlight how even just a surface level exposure to these principles has positively impacted my day job.
Learning a Functional Language
After reading about functional programming and how great it is, I just had to try it out! There are plenty of languages to choose from, but I decided to go with Haskell mostly because it’s a pure functional language (as opposed to impure functional languages, which support imperative style programming). I figured if I’m going to start learning I might as well go with the “most” functional one. I’m still not sure if that was the best decision or not but I don’t think it matters very much!
The first thing I realized is that the learning curve is pretty steep. I started by going through Real World Haskell and What I Wish I Knew When Learning Haskell, among others. After getting through the basics, I started to get a little overwhelmed. I knew this was going to be different and I was excited for that, but I was utterly shocked at how many words I was reading for the first time! In my last post I wrote briefly about how I learned Rust. I think part of the reason I was so shocked is because I was able to feel comfortable in Rust quickly. It has a similar syntax to C++ and other imperative languages I have used so it was not a huge adjustment. Additionally, there were only a few genuinely new concepts that I had not heard of before. Contrast this to Haskell where I felt like there were only a few concepts that I did recognize. I (naively) was not expecting that after ~6 years of programming, but hey that’s why I fell in love with coding in the first place!
Despite my struggles, I continued to read and write Haskell code. I firmly believe that learning new ways to solve the same problem can open your eyes and give you fresh perspectives which generally improve your problem solving abilities. I experienced this when learning Rust, and even though I am starting from scratch when it comes to functional programming and Haskell I have already noticed how it influences the way I write C++ code.
- After spending just a brief amount of time coding in Haskell, I now know what it feels like to truly have no global state. Now, C++ obviously doesn’t enforce these rules like Haskell does, but having that experience has encouraged me to incorporate it into the way I write C++. Instead of a class method using a member variable to manipulate state, I may try to create a struct for inputs and a struct for outputs so each function call is acting on a separate piece of data. This allows me to run the code in parallel much more confidently. This is a huge plus for me since I deal with a lot of multithreaded code!
- I also have a new-found appreciation for const-correctness due to having truly immutable data in Haskell. It’s very helpful to think about whether or not you expect data to be modified in each C++ function and mark it as
constaccordingly. It’s not always obvious when variables are modified, so it’s much better to put that burden on the compiler.
Granted, these may be habits I’d learn to gravitate towards anyway as I write more C++ code, but using a language that places such a strong emphasis on core tenants such as immutable data was a strong motivator to start incorporating them myself.
What’s the catch?
After being introduced to functional programming, it seemed to be some sort of silver bullet that should be used for everything. I started asking myself a question that you’re probably wondering right now:
If functional programming is so great, why doesn’t everyone use it?
As with anything, there are trade offs. For instance, in the example above when I mentioned creating input/output structs instead of using class member variables to pass between functions, I’m trading conciseness and a stronger thread-safety guarantee for space. If my function inputs or outputs are very large, it may be too costly to create fresh copies of the data structure on each invocation of the function based on the time and space constraints.
The main issue is that it’s impossible to do anything of value without having some side effects. Printing to the console, reading/writing to a socket, and creating a file are all forms of side effects. It seems to me that there is a sweet spot between a fully pure language and a language that can handle side effects, and therefore produce business value. Perhaps that sweet spot has not yet been found by many companies, which could explain why functional programming has not yet gained a foothold in the corporate world.
Despite the drawbacks outlined above, I’m optimistic about functional programming, and I think it’s only going to become more relevant. I’m excited to “start from scratch” with something brand new. Reading Greg Michaelson’s Introduction to Functional Programming through Lambda Calculus and Bartosz Milewski’s Category Theory For Programmers are providing plenty of difficult, new topics to think about while I program. It’s going to take a while to feel comfortable but I hope to write about more specific topics in the future and how they make me a better programmer. Thanks for reading!