When people think about functional programming, two things come to mind. The first one is usually pure function; the second one is immutability. While these two are correct, the key to thinking in functional programming is to think in a declarative way.
A declarative way of thinking about programs is where you express a computation logic without describing its control flow. It is also how humans usually speak. Let me give an example - when you are thirsty, and you want to drink water, you don’t tell others than “I want to fill up water on a cup, and chuck it on my mouth”; you tell them that I want to drink water. You are essentially declaring to other people what your intention is instead of how you wish to achieve your purpose. The what is declarative.
On the other hand, thinking in terms of imperatively is when you think about how to accomplish the computation logic. Your focus is on the state of the application - you have a list of commands for the computer to perform. Usually, we see this a lot when we look at recipe books. It doesn’t tell you what the intention, but how you want to achieve that intent. Imperative programming focuses on how the program should operate.
In terms of writing programs, they have various ways of writing programs. A declarative way of coding programs tends to be more recursive. Conversely, the imperative form of writing programs usually requires loops. For instance, to get the sum of a list, you will have for loop and add each item on the array to a variable. The declarative form of doing this will be to looping each element’s recursive way, and on each component, we want to add each other.
While most of us usually started imperatively writing code, I want to show the other way of how you can write your code to be more declarative so that your code can be more descriptive.
This article will not talk about both paradigm’s pros and cons but to give you an idea of how you can program your program declaratively. These are four functional operations that I often used as I got into the functional programming journey that you can use in your tool belt to write programs declaratively. I will not talk about
filter since it has been widely used in other functional languages.
Fold takes data in one format and gives it back to you in another. Any data processor can condense a lot of the operations in all these programs to fold. For instance, reversing a list - the imperative way will be to do a two-pointer and a for a loop.
The declarative way will be using
foldLeft, like this:
Fold takes two arguments; the start value and a function. This function also takes two arguments; the accumulated value and the current item in the list. Therefore, you can describe what you want to do on each of the iterations by specifying that you want to append the
curr to the rest of
acc - which reverses the existing list.
What if we want to stop in the middle of the list. How can we represent a while loop?
Take and Drop
Take and drop operation is what you expected it would do. If there is a list of elements, and you said you want to take the first two. You can do it like this:
Conversely, if you want to drop the first two and want to take the rest - you can use
Certain algorithm operation in Scala might be hard to implement without a recursion. For instance, if you want to split a list into two parts based on some index, one way to do it is using a recursion or a for loop. Like this:
You can also
drop to take the
index value and
drop the index value :
The zip function is to merge two sequential collections into pairs. This operation is beneficial when you want to make two separate collections into a pair, especially in a stream environment when you want to join both streams or make both streams run simultaneously.
If you want to do a stream of infinite fibonnaci sequence, you can call it with a zip function:
The Fibonacci stream zips two-stream - one including the head of the list, and the other doesn’t include the head of the list. In this case, fibs will start with
fibs.tail will start with
[1,1,2,....]. We combine the two-stream together -
[(0,1), (1,1), (1,2), ...]- and add them together. The result will be a list of the Fibonacci sequence.
Unfold is the opposite of fold. Instead of recursively condensing the sequence into a single value, it reverses the logic by taking the initial state and building both the following condition and the next value in the sequence to be generated.
A first glance at the method signature might make you think how to unfold could be possibly useful. However, unfold is a very generic stream building function. The unfolding concept is co-recursion, which lets you start from the base case to slowly build-up to the next value. We can try to use the Fibonacci sequence from the
zip example to construct
The above function takes in the initial state, a tuple of zero and one, and execute the process to “unfold” the sequence. In each iteration, the returned tuple of type
- The first element
Ais the value to be added to the resulting sequence - in this case, it will be zero.
- The second element
Sis the next state value - in this case,
Sis another tuple (one, zero+one)
- A return of
Somesignals that the function generates the new element. A
Nonesignals the “termination” for the sequence generation operation
I hope you get to put these functional operations under your tool belts to write programs in a more declarative way. We are just scraping the surface of available functions. Other active operations have been instrumental in describing the intent of your plan. For instance, FlatMap is a combination of map and then flatten the nested sequence; dropWhile and takeWhile are both functional operations on top of taking and drop, including additional predicate; zipWithIndex will zip the sequence with an index as a tuple. These operations are more powerful and build in a more general way. Both paradigms have pros and cons, and each has its use cases. Using these functional operations helps you make the intention of your program more composable. It lets you state what you want to do with your program. On the other hand, using an imperative way of writing the program can help make your program more modular - it improves the program’s maintainability. Now that you know this functional technique of composing your program stop using regular recursion and use these declarative functions operations to make your code more readable.