Switching to Functional Programming: Keystones and Advantages
Understanding the Shift
For many of us, starting out in the world of programming is a matter of choice. So we’re guided by ideas like language popularity, community, available tools (including documentation) and ease of learning.
Because of this, it’s likely that we will be drawn into the world of imperative programming, as most popular languages fall into this category.
But after spending months or years exploring the coding landscape, something just doesn’t seem right. Bugs and issues are hard to spot, the code base is growing with chaotic classes and large modules, and there is a lack of clarity in functions and operations.
This is not solely because of imperative languages, of course. However, when you delve into the functional paradigm, many things start to click and become clearer.
Let’s understand why functional programming can help you in the code improvement journey.
Advantages of Functional Programming
Pure functions
Pure functions are easier to write and test: they don’t modify external variables or mess around with state. They perform one action and always return the same value for a given input. Thus, functions are considered to be free from side effects, making them easier to reason about and test.
Functional programming promotes the use of these functions, since predictability simplifies debugging and testing. Then we can be confident that a function will behave consistently, regardless of the context in which it’s used.
Implementing pure functions involves adhering to some guidelines to ensure their reliability and predictability. Those must maintain immutability, meaning they should not modify existing variables or data structures.
Instead, we create new objects with updated values, preserving the integrity of the original data. Additionally, these functions should avoid external state entirely, ensuring they operate solely on their input parameters.
This excludes reliance on global variables, I/O operations, or randomness. Pure functions should not engage in actions like reading from files, performing network requests, or using random number generators, as these activities introduce unpredictability and violate the function’s purity.
Also, pure functions should not rely on time-dependent operations and exceptions, ensuring consistent behavior across different contexts.
To uphold their purity, these functions should not manipulate shared mutable state, and any interactions with shared state should be carefully controlled to prevent unintended modifications.
By taking these points into consideration, we are on the path to achieving functional composition, which allows the construction of more complex behaviors by combining multiple functions, thus promoting modularity and code reuse.
A quick example of such function is as follows:
(defn is-prime? [n]
(and (> n 1)
(not-any? #(zero? (mod n %)) (range 2 (Math/sqrt (inc n))))))
The is-prime? function above takes a number n as input and returns true if n is prime and false otherwise. It operates solely on its input and doesn’t rely on or modify external variables, making it a pure function.
First-Class and Higher-Order Functions
In other words, functions as first-class citizens. They can be passed as arguments to other functions, returned as values from other functions, and assigned to variables.
This flexibility enables the creation of functions, which take one or more functions as arguments and return a new function. Higher-order functions are the keystones to promote composability, as we’ve explained before.
A simple example of a higher order function usage is filter. See how it’s used:
(def numbers [1 2 3 4 5 6 7 8 9 10])
(defn even? [x]
(even? x))
(def even-numbers (filter even? numbers))
;; Result is [2 4 6 8 10]
The function even-numbers calls filter, providing another function even? as the method for selecting the items in numbers list.
Here we see the function even? being passed as argument to filter, and the result of this operation is also returned to even-numbers.
Notice how easy is to read code like this, which splits operations on smaller and simpler functions.
None of these functions are depending on the implementation of each other; they only expect the output of each operation.
Immutable Data Structures
In order to achieve a higher level of purity and integrity, a language must provide structures to allow immutable data, meaning it cannot be changed after it’s created. This immutability simplifies the program state.
Instead of changing existing data, operations on immutable data create new data structures, leaving the original data unchanged. This inherent immutability provides numerous advantages, especially in the context of handling complex data.
Clojure provides a rich set of immutable data structures, allowing you to create complex data structures without the fear of unintended side effects.
Take the code below as an example of map and the assoc function:
;; creating an immutable map
(def person {:name "Jeremy" :age 43 :city "New Hampshire"})
;; using assoc will create a new map, preserving the original data
(def older-person (assoc person :age 87))
;; older-person is now a copy if person, with a different age, while
;; person remains the same
In this example, the assoc function associates a new value with a key in the map, creating a new map – older-person without modifying the original map, person.
Benefits of immutable data
Predictability
Since data doesn’t change after it’s created, you can trust that a piece of data retains the same state throughout its lifetime. This simplifies the mental model of your program, making it easier to reason about the behavior of your code.
Knowing that your data is immutable allows you to focus on understanding the flow of data and the sequence of transformations, rather than worrying about unexpected changes due to mutable state.
Simplified Debugging
When a bug occurs, you can confidently trace the flow of data through your program without being concerned that the data might change unexpectedly.
This ease of tracking data flow is especially valuable in complex applications where understanding the state of mutable data can be challenging.
Immutable data acts as a constant reference point, making it easier to identify the source of bugs and ensuring that fixes don’t inadvertently introduce new issues due to mutable state changes.
Concurrency and Parallelism
Immutability brings the benefit of thread-safety, making these structures ideal for concurrent and parallel programming. In multi-threaded environments, shared mutable state can lead to race conditions and complex synchronization problems.
Immutable data, on the other hand, eliminates the need for locks and explicit synchronization. Different threads can work with immutable data structures independently, avoiding conflicts and ensuring the integrity of the data.
In conclusion
This short article is far from covering all the aspects for which you can take full advantage of functional programming, even to apply its techniques to other languages that aren’t functional by design.
My goal here was to provide a starting point to hopefully spark in you the desire to learn more about functional programming—not necessarily as a change of paradigm but as a means to increase your skillset and bring your code reasoning to another level of awareness.
Glad you made it until the end! If you have something to add or just want to say hello, feel free to share your thoughts. See you in the functional Valhalla.
Write a comment