Paradigm Shift
Introduction
Fortran was the first high-level programming language I learned in college, followed by Pascal and then C. The book by K&R was my bible, and I mastered pointers to show off among my friends. Unix was the only operating system I had access to, and I loved writing low-level drivers. Over the years, I worked with C++, Java, and various scripting languages such as Perl and Python, depending on the task at hand. I love coding, but given my extensive experience in the software industry, I can offer more than just churning out PRs to climb the GitHub leaderboard. In the past year, I’ve pushed more code than I ever anticipated because, in a startup, you do what you have to do. Most recently, I was tasked with delivering a feature that required writing code using Scala. Why Scala? I suspect because the engineers working here were comfortable with it. I’ve been coding in Python, and it has been almost five years since I coded in Java. So, learning a new language was exciting but also stressful, especially when needing to fix or add a new feature in a short time. Additionally, the code I was working with was functional in nature. While recent versions of both Java and Python support functional programming, I lacked experience with it. In this post, I will share my journey into Scala with focus on Functional Programming and the benefits I've discovered.
Programming Paradigms
When I first tackled the task, my go-to resource wasn’t a traditional book on Scala or programming paradigms; it was GitHub Co-pilot. Co-pilot was invaluable in helping me understand existing code and generating code for generic tasks. However, as I delved deeper, Scala’s syntax baffled me, and some concepts felt counterintuitive. This sparked my curiosity to learn more about functional programming and its nuances.
A programming paradigm is a style or approach to programming that provides a set of concepts, rules, and abstractions for solving problems. It offers a blueprint for how to approach a programming task. There are many paradigms, including Imperative, Declarative, Functional, Object-oriented, Concurrent, Event-driven, and Reactive, to name a few. While I have experience with some of these, declarative and functional paradigms were new to me.
Imperative programming focuses on describing how to achieve a result step-by-step, whereas declarative programming focuses on what the result should be, not how to achieve it. Functional programming, as a specific implementation of the declarative paradigm, achieves this by using functions as the building blocks and avoiding mutable states. For example, in imperative programming, I’d outline how to iterate over a list and manipulate its elements. In functional programming, I’d define a transformation function and apply it to the list. In other words, instead of providing a step-by-step recipe to complete a task, you simply specify what needs to be done. This shift in mindset is what made the learning process challenging but also rewarding.
Embracing Functional Thinking
Functional programming is a programming paradigm that emphasizes writing pure functions, which always return the same output for a given input and do not cause side effects. This approach focuses on immutability, higher-order functions, and recursion instead of loops and mutable state. Functional programming promotes a declarative style of coding, where you define what to do rather than how to do it, making code easier to reason about, test, and maintain. Functional languages like Haskell and Scala are designed to support this paradigm, while other languages like Python and Java can also support functional programming to varying degrees.
There are many resources that cover Functional programming and will leave it to the reader to explore them. In this section, I will focus on some of the challenges that I encountered while adopting functional programming, especially since I’ve more experience with imperative and object oriented programming.
Mindset Shift
Functional programming requires a different way of thinking. Instead of focusing on how to change state through a series of steps, you focus on defining functions that transform data. This shift from a step-by-step, procedural approach to a more declarative, expression-based approach can be challenging.
Immutable Data Structures
I've always been mindful of memory management, especially having worked with embedded systems. Keeping track of and freeing unused resources is crucial to prevent performance degradation. Additionally, in imperative style, variables are often modified throughout a program. In contrast, functional programming emphasizes immutability, meaning that once a data structure is created, it cannot be changed. Adapting to immutable data requires a different approach to problem-solving and often involves more complex data manipulation techniques.
Higher-Order Functions
In functional programming, functions are first-class citizens and can be passed around just like any other value. This introduces the concept of higher-order functions—functions that take other functions as arguments or return them as results. Passing function pointers was common when I worked using C as the programming language. I found the old K&R book that I used when in school here. I highly recommend checking out the section on Complicated Declarations in this book. Understanding and effectively using higher-order functions can be difficult for those unfamiliar with this concept.
Recursion over Iteration
Functional programming often favors recursion over traditional looping constructs like for or while. Recursive thinking can be tricky, especially when it comes to avoiding common pitfalls like stack overflow errors in languages that don’t optimize tail recursion. Tail recursion is a powerful optimization technique that helps improve memory efficiency and prevents stack overflow errors. To ensure that a function is tail-recursive, the recursive call should be the last operation performed within the function. This often involves rewriting the function to use an accumulator parameter to accumulate the result.
Pure Functions and Side Effects
Functional programming focuses on pure functions, which always give the same result for the same input and don’t change anything outside themselves. This can be a big change, especially if you’re used to programming in a way that often involves changing things like global variables or interacting with other parts of your program.
Lazy Evaluation
This is a technique in programming where calculations are done only when they're actually needed. This can make your program run faster by skipping unnecessary work. It also allows you to work with endless lists of data without using up all your computer's memory. Plus, it can make your code easier to write and understand by letting you describe what you want to do in a straightforward way.
Note that yield is a keyword in Python and is required to pass the execution and return the value. Without it, the above function will run to completion and return a single value.
Functional Pitfalls
Functional programming introduces abstract concepts like monads, functors, and other types, which can be complex for beginners. The ecosystem for functional languages may not be as mature or familiar as that of imperative or object-oriented languages, making it harder to find tools and libraries. While strictly not necessary, itertools is a powerful module in Python that provides a collection of iterator-based functions, making it a valuable tool for functional programming. Additionally, functional programming can lead to performance overhead, requiring developers to learn new optimization techniques, tail recursion, and balance purity with efficiency.
Conclusion
Functional programming is different from what you may have done so far. No matter how long you've been programming, you must be prepared to become a beginner once again. It's important for anyone who wants to be considered a software professional to know multiple languages and various programming paradigms. In less than three weeks, I was able to deliver a feature for one of our products by implementing it in Scala. Although I have prior Java experience, it still took effort to code in Scala. I was often inclined to write code in an imperative style, and I had to consciously shift to functional thinking. I hope this post inspires you to explore different programming paradigms and step out of your comfort zone to learn another language to add to your toolbox.
Before I end this post, I’d like to draw an analogy between coding in Scala and leading a team of engineers. As a leader, it's essential to delegate by trusting your team members and empowering them to accomplish tasks independently. This is similar to applying functional programming principles: delegate tasks by defining the desired outcome without prescribing the exact steps to achieve it. Just as in functional programming, where you specify what needs to be done rather than how to do it, effective delegation involves giving your team the autonomy to determine the best approach.