Programming is hard. Human brains are not wired to work like computers. Humans also have limited cognitive resources. Computers are only limited by the hardware they are built with.
Regardless - until we reach the singularity - humans are responsible for programming computers and as a whole we are not doing a very good job of it. There are many reasons that our software is bad, but I want to focus on solving the complexity issue.
You see humans have a very limited amount of cognitive resources - which makes trying to reason out (“grok” if you will) a program very difficult. In fact we humans are even more limited in cognitive resources than you might think:
While the focus of that video revolves around utilizing our resources better when trying to learn software (which is a fascinating study in itself) - it demonstrates the scarcity of our cognitive resources.
Software systems are only becoming more complex. So how do we keep up without wasting resources and writing bad code?
Abstraction and Immutability
When was the last time you wrote some assembly code? I sincerely hope the answer is never because we have high-level programming languages that compile and abstract the actual machine code that gets processed by the computer therefore preserving our cognitive resources.
Have you been writing object-oriented code? This is yet another abstraction that models computer solutions in terms objects and their relationships. As humans we grasp this paradigm so much easier than procedural coding because the model reflects our physical world and their relationships. Our brains see familiar patterns and understand them.
You may at this point be saying to yourself “Sure but we’ve had high-level languages and object-oriented programming for years now - yet programming is still hard and we’re still writing buggy software”. One of the biggest problems with object-oriented programming is that it encourages mutable state within objects (see below), but we can resolve that. Programs are so complex now that OO abstraction is no longer robust or abstract enough for humans to grok effectively. So let’s abstract even higher!
What I’m talking about here is writing declarative code versus imperative code. Writing code imperatively means giving the computer step by step instructions to perform in a specific order. Writing declarative code is more like abstractly describing a problem and what you would like to see as the solution - then letting the computer figure out the best way to solve it. That may seem somewhat strange to you - but you’ve most likely been using some declarative programming without realizing it. If you’re ever written any SQL - then you my friend have written declarative code.
Examine the following SQL statement:
If I were to re-write this imperatively in pseudo-code I might have to do something like this:
Isn’t declarative so much cleaner and easier to understand? In SQL - I just describe what I want and let the database code figure out the most efficient way of getting the data back to me. Such nice abstraction! Why are we still writing imperative code in our applications? Wouldn’t it be nice to write declarative code and let the computer figure out the most efficient way to execute the instructions?
I’m going to make a completely unscientific and unqualified assertion that 82.3% of the bugs in your object-oriented code are caused by mutable state.
Seriously though - why do we even use a debugger if not to examine the state of objects at certain points in the application’s execution trying to figure out how/where/why it’s changing.
In addition to bug-prone code, trying to reason out a program with mutable state severely drains your cognitive resources. Even with a debugger, trying to juggle the current state & interaction of all those objects in your head will take its toll.
If you’re wondering how you can do anything useful without mutable state, you’re not alone. Rest assured it’s possible and effective. It just takes a bit of re-training for your brain not to think that way. That stack overflow article is very helpful, also there’s this library (read the case for immutability) and this article, and this one about immutable objects.
By now hopefully you can see the benefits of abstraction, declarative coding, and immutability. So where is this magical land of unicorns and rainbows where we can write simple robust software using these constructs? Believe it or not the answer is not a new language or another framework. It has actually been here all along, silently waiting for us to dust it off and realize its full potential.
In computer science, functional programming is a programming paradigm — a style of building the structure and elements of computer programs — that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. It is a declarative programming paradigm, which means programming is done with expressions. In functional code, the output value of a function depends only on the arguments that are input to the function, so calling a function f twice with the same value for an argument x will produce the same result f(x) each time. Eliminating side effects, i.e. changes in state that do not depend on the function inputs, can make it much easier to understand and predict the behavior of a program, which is one of the key motivations for the development of functional programming.
- Functions are pure: Functional purity means that a function given the same input will always return the same output, and executing the function produces no side-effects.
- Functions do not store or mutate state: This is really just a follow on to the last point. The only reason for a function to store state would be to use it in subsequent calls which violates it’s purity. If a function mutates state outside of itself - that is what we call a side-effect which is also impure. State changes in a functional program are modeled by passing a data structure into a pure function and receiving a new data structure as the result. This new data structure is completely predictable (read testable) by nature of the function’s purity given the input.
- Functions are first class citizens: Essentially this means you can treat functions just like any other code construct. ie: you can store functions in arrays, assign them to variables, pass them around (even as arguments to other functions!).
- Functions are _compose-able: _This feature you get by nature of the previously listed facets, and is perhaps the most important feature of functional programming. This is where that higher level of abstraction comes into play. Let me try to demonstrate with this (admittedly straw-man) scenario:
First lets say I’ve created a pure function yToZ() that converts ‘y’ to ‘z’.
Great. That’s a nice pure function. Since we’re good developers we write a suite of unit tests against that function. Now we’ve built up our confidence that yToZ() is a solid function that will have expected out put without side effects (remember it’s pure). In fact I’ve built up enough confidence that I don’t even care how the implementation of yToZ() works. All I know is that I give it an ‘y’ and I get a ‘z’ every time and nothing else in the program gets touched. And now just freed up all those cognitive resources that were being used to understand how yToZ() works.
Uh -oh here comes Manager Bob with a feature request - now we need to have a program that converts ‘x’ to ‘z’. Great.. now I have to write a brand new function ‘xToZ’… or do I? I think to myself ‘x’ is pretty close to ‘y’ and I’ve already got yToZ(). I don’t remember how it works but I know it takes a ‘y’ and returns a ‘z’. Let me create xToY() and see where that gets me.
Now of course I write my unit tests for xToY() and forget about its implementation and free up my cognitive resources again.. I don’t remember or care how these functions work , but I know what they expect and what they output. Now wouldn’t it be nice if I could combine (compose) these two functions to get from my input ‘x’ to the desired output ‘z’.
Nice! Now I have xToZ()! I’ll write my unit tests - and free up cognitive resources. I don’t have to remember that xToZ() is actually composed of two separate functions. I just know that I give it an ‘x’ and I get a ‘z’.
three months later…
Manager Bob : users want to give us a ‘w’… but they still want a ‘z’ returned. At this point I have no recollection of how xToZ() was implemented, and i don’t care. All I have to write is a wToX() and compose them and I’m done… and so on…
Is functional programming going to solve all your problems and make all of your bugs disappear? Obviously not. Additionally it’s a bit of a steep learning curve - especially if you’ve been programming imperatively for a long time. You will need to re-train your brain into a somewhat drastically different way of solving problems programmatically. Also, functional paradigms can sometimes seem a bit too academic and theoretical. That’s because most functional concepts are backed by solid mathematical axioms (Category Theory) and usually expressed in mathematical notation. You might start to see words like Functors, Monads, chain, and foldMap. Don’t let it scare you away. All you need is a good teacher and a bit of effort. It’s well worth that effort. Might I very highly recommend the following:
Functional programming is nothing new. However, it’s popularity is rapidly increasing and I predict a functional renaissance in the very near future. Let’s stop wasting our limited cognitive resources writing difficult code that’s hard to maintain and hard to understand. Let’s write simple declarative code and start building our Lego creations.