What's this fuss about functional programming
Functional programming is not new although you could be forgiven for getting that impression from following programming news, conference talks and other such outlets. Some of the very first programming languages were functional in the sixties but the rise of easy-to-get-started imperative and then object oriented languages (still in the imperative style) overwhelmed it so completely that functional style was for a good while forgotten from the mainstream.Now the phoenix is rising from the ashes and wondering why everyone thinks it's new and shiny. Or why it burned out in the first place.
I agree with Venkat Subramaniam in that actually functional programming is not consequential when you compare it to the larger category it actually belongs to which is declarative programming which, at its base, describing what you want to achieve instead of the steps of how to do it (referring to conference talk https://www.youtube.com/watch?v=uQ75fI1tqoM and associated blog post https://contemplative-architect-journey.blogspot.com/2019/11/speed-without-discipline-recipe-for.html). But this is all categorization and wordplay, what is the actual benefit?
What's the benefit - the state of the functional
Functional programming isn't at its very core so much about functions as it's about how we handle state. Which in the functional / declarative world means pretty much staying away from it as much as possible, and when you do have to deal with it, treat it as if it were toxic (which it is, which makes you take care).
"But how can you develop without state" is the natural next question from anyone not versed in the secrets of the immutable mindset.
Consider the following simple code example in Java (borrowed from one of Venkat's talks)
public static void main(String[] args) {
List<integer> numbers = Arrays.asList(1,2,3,4,5,6,7);
int total = 0;
for(int i = 0; i < numbers.size(); i++) {
if(numbers.get(i) % 2 == 0) {
total += numbers.get(i) * 2;
}
}
System.out.println(total);
}
List<integer> numbers = Arrays.asList(1,2,3,4,5,6,7);
int total = 0;
for(int i = 0; i < numbers.size(); i++) {
if(numbers.get(i) % 2 == 0) {
total += numbers.get(i) * 2;
}
}
System.out.println(total);
}
What is happening here? This is a simple example and it's easy to deduce the intent behind the algorithm but still it takes a little moment.
How do you mentally debug this? It requires you to hold in your mind the state of the different variables of which the relevant block is composed and which could change and then mentally execute the logic.
Being an example, this is simple but what about the real world situations where there are actually 20 different variables that could change, that could be in different settings based on at which situation you're thinking of the algorithm, which method calls have been made and what may have been altered from outside the context you're thinking of. And then add a couple of non-intuitive language features or method implementations (why does that get method alter the object's internal state?) and you've got a mess.
This is the situation developers refer to when they tell about being deep in thought debugging a problem when someone interrupts their chain of thought with a question or email or skype message or cat problems and to continue they need to build that mental state image all over again.
Now would you hazard to make a guesstimate on how many bugs and problems this leads to? Whatever your guess, I can make the educated guess that it still under-estimates the problem.
So how does functional style help? It does away with state mutation entirely unless it's absolutely necessary- and you would be really surprised how rarely that is the case when you come from purely imperative background.
Let's take the same example from above and write it in functional style supported by contemporary Java:
public static void main(String[] args) {
List<integer> numbers = Arrays.asList(1,2,3,4,5,6,7);
System.out.println(
numbers.stream()
.filter(e -> e % 2 == 0)
.mapToInt(e -> e * 2)
.sum());
}
The difference is that now you can look at each individual step in isolation from everything else and know that it takes in and what it returns without having to consider the state of everything else. In this oversimplified example the benefit is not considerable - and if you're not used to this style then the overhead of mentally parsing what's going on is actually higher than the more familiar and traditional imperative style but imagine what the effect will be in the cases where there would traditionally be 20 variables whose state you have to mentally monitor to be able to understand what is going on in the code. Or 50. Or 100? At some point you just can't deal with that. No one's mental capacity is sufficient. And also no matter how the rest of the codebase changes when you run these operations again, it still returns the same answer (reproducibility).
This is the essence of the functional style.
You can also consider an additional factor - read the code aloud from the first and second examples and think how it relates the intent and requirements behind the implementation. In the functional example you can read the lines "get stream of numgers", "then leave the even ones", "then multiple each by two" and finally "and get the sum of those". The reading of the code reflects the intent and the requirements as is while in the imperative example you need to tease and parse out the intent from how it behaves - and even then you can make mistakes (which is why commentary in imperative still is imperative, pun intended - and of course often gets written to be misleading, especially after updates to code and not to commentary).
This is the essence of the declarative style.
You would be surprised how much of the stateful imperative code can be phrased in an entirely immutable fashion.
I have personally experienced the transformative effect this can have on your code quality. While in my imperative object oriented days I would routinely write 10 to 20 lines of code, run it, encounter a bug, fix it, encounter another and after another fix or two get it working as intended. After my personal functional revolution I have many times written as much 500 lines in one go (some of it non-trivial, and still in the same language as before), ran it and it just worked (your mileage may vary). It's just amazing how much your productivity can increase when, after creating the overall structural plan in your mind, you can concentrate on just the current line / function in your implementation and not have to worry about the rest.
It's my view that the reason declarative and functional styles work so well and provide such benefits is the imperfection of the human mind. We have a very limited working memory so the more we can segregate orthogonal concerns to separate threads of thought and focus on a highly (and truly) restricted subset, the better we can do. The best coding styles create structures of a size which our minds are capable of handling (and not just in the happy path case but in the overall complexity of the section of code you're looking at with absolutely everything that could affect it).
So is Java functional or not? What about C?
Functional and declarative are styles, not languages. Almost any programming language is capable of benefiting from functional style of programming even when they do nothing to actively support it (while there are e.g. some basic variants etc. which don't enable it in any sensible way - and still it's possible to get some benefit). Then there are what are known as actually functional languages which to some degree enforce functional approach (Haskell to a very high degree combined with static typing, Clojure and other Lisp variants to a slightly lesser degree from the dynamically typed world, etc.) or at the very least provide extensive native tooling for it and treat it as the default (Scala, F#, etc.). We've also seen the trend of mainstream object oriented languages having more functional capabilities incorporated (Java, C#).
The question of "What is a functional programming language" is (outside of edge cases) more of a terminology issue. What is the more interesting approach is to what degree can you benefit from taking a different approach compared to your current one from learning a new way of thinking with your current tooling. And is there a way to affect your tooling to improve your outcomes based on what you learn.
You can bring the benefits on this immutability / functional thinking even into languages that traditionally get categorized as entirely imperative such as C. I remember seeing a talk by John Carmack (original Doom's programmer) on how he transformed his C programming with functional style which resulted in significant drop in bugs and increased development speed. With some quick googling I could only find his article on the topic relating to C++: https://web.archive.org/web/20130819160454/http://www.altdevblogaday.com/2012/04/26/functional-programming-in-c/
But functional programming is hard - and how to make it less so
The hardest kind of change is when you have to acclimate yourself to an entirely new kind of thinking. In programming one of these transformations is from the imperative to the declarative styles (and as a subset, from object oriented to functional). That does take some getting used to.
But no one forces you to drop Java and go 100% Haskell - a total 180 is hardly beneficial given all the frictions it comes with (especially if mandated from up high).
To start getting benefits from the style of thinking and development approach highlighted above, you don't have to go 100% into it immediately.
What I recommend is you start familiarizing yourself with one functional focused language from your domain (I recommend Clojure for Java stack folks and F# for .NET stack ones) and begin trying to build some practice program after watching a few tutorials. Then do some more tutorials and practice programs. After you feel like you're starting to get the hang of it, find the corresponding stream / LINQ functionalities from your familiar language of choice and start experimenting with making everything your code immutable by default and see how far you get.
This way you don't have to upend your world at once (which wouldn't be practical anyway). Instead you can perhaps start getting some view of what the benefits are after repeatedly hitting your head against the wall when trying to accomplish the Clojure / F# exercises.
And perhaps later you'll decide to challenge yourself and your belief in your superior abstract thinking, by learning Haskell. You will feel stupid. A lot more of wall-head-contact ensues (assuming you haven't given up) but you keep accruing valuable insight all the way. And mentoring and uplifting your colleagues on the way no doubt (hopefully in a positive fashion)
Immutability beyond the lines of code - the architectural perspective
The last few years it has been very interesting seeing how the immutability principle and declarative style has been brought to higher and higher levels of abstraction. Immutability is the basis of event sourcing, it is an important component in the philosophy of build-once-deploy-everywhere CI/CD pipelines, declarative CI/CD pipelines (increasingly the de facto standard of CI/CD), immutable data engineering (https://medium.com/@maximebeauchemin/functional-data-engineering-a-modern-paradigm-for-batch-data-processing-2327ec32c42a - Airflow, etc.), immutable architectures, containers (basis of OpenShift security practices), etc. etc. etc.
It is literally everywhere providing improved decoupling and improving your (the developer's) ability to focus on one single thing at a time and actually and fully understand what is going on in any specific context you pay attention to.
In short, the declarative style enables (or at the very least maximises the chance of) you to understand what is going on without having to keep the entire context of your application or architecture in your mind and pretending to actually understand the systemic complexity of it, black swans et. al.
Referenced conference talks
https://www.youtube.com/watch?v=FQERMVABRrQ
https://www.youtube.com/watch?v=uQ75fI1tqoM
No comments:
Post a Comment