Functional Programming Made Easy with Analogies

·

12 min read

Functional Programming Made Easy with Analogies

Introduction

Imagine you are a detective trying to solve a crime. You have a list of suspects, each with different characteristics like age, hair color, height, past criminal records, and so on. Your task is to find the criminal who fits the profile.

Now, one way to do this is to go through the suspect list one by one, checking each suspect’s characteristics against the profile. This is like following a set of step-by-step instructions.

💡
Finding criminals using step-by-step instructions, we can call this style an imperative style.

But what if there was a better way? What if you could ask for the end result you want? What if you could simply describe the profile you’re seeking and simply state, “Find me a suspect who is over 5' 6" feet tall, has black hair, and is under 40 years old,” and get the criminal instantly?

💡
Finding criminals by just describing the profile, we can call this style a functional style

Now before you get all Sherlock Homes feeling and get detective let me share the news, "We have to get functional as we have a road trip ahead !!" But before we start, I think we can have a look at the simple definition of both realms. I promise I will not make it bookish and boring so stay with me. Most of us are quite familiar with the imperative style. Let's revisit it once 👓.

Imperative Style

The imperative style is a step-by-step process of explaining what do to and how to do it. Programmers must articulate clear steps and conditions to solve any given problem. Here are the steps taken to solve any problem in this style.

  1. Define a detailed step-by-step plan to solve the problem.

  2. Use loops, conditional, mutable state variables, and statements to solve the problem.

  3. Focus on the "what" and "how" of the solution, specifying each operation precisely.

You may recall the introduction about finding the criminal using step-by-step instructions !!

Functional Style

Here are the steps taken to solve any problem in this style.

  1. Focus on the "what" part of the problem.

  2. Utilize higher-order functions to process data to solve the problem.

  3. Compose smaller functions to build a more declarative and reusable solution.

Before you start drifting with the doze of definitions, please don't !! We are going on a road trip !! hurray !!. Pack your bag and get some snacks.

A Road Trip Using Both Styles 🚗

💡
A Trip without GPS: Imperative Style

You are on a road trip from Bangalore to Pune. It's a route you have never traveled before. You don't have a phone or GPS with you...wait, wait..you might ask, why in the name of GOD I don't have a GPS? Let's just say you traveled back in time using the time machine for some crazy reason when GPS wasn't invented or your battery is dead or you lost your phone, who cares 😉!!

Now, coming back to our trip, without GPS, you might have to rely on roadside signs or you might ask locals for directions and make decisions along the way based on available information. Your trip totally depends on the decisions you might be making !! One wrong turn and you have to start your journey all over again. This is what you do mostly when you work with imperative style. When you have to solve a problem using imperative style, you follow steps and use conditions to take decisions. For each given condition you define a different flow of handling the situation. It focuses on what and how part of the problem.

💡
A Trip with GPS: Functional Style

Note: Don't worry if you are viewing this image on a laptop and wondering if this could have been adjusted better. Forgive me, I am not perfect !!

Let's get ready for the next road trip. What if you had a GPS device? You might have it in your pocket right now or using it currently !! As you know, it's a long trip so I am trying to crack some jokes, clearly, I failed !! Let's just focus on the driving.

How do you make use of GPS while traveling, if I may ask? Easy peasy right, Before you start your journey, you set your current location, enter the destination, and let GPS handle the rest. It provides clear directions, turn-by-turn instructions, and the ETA of the trip. You can explore unknown territory without relying on external cues or directions. There is no risk of accidentally taking a wrong turn due to some wrong decision. Similarly, in functional-style programming, there is no risk of accidental errors in the code as functions are pure. You can just focus on what you are trying to solve and how part will be handled by higher-order functions. Since I have introduced two new terms i.e. pure-functions and higher-order functions, it's time to know some basic concepts of functional style.

Concepts of Functional Style Programming

Pure Functions :

  • Always returns the same output for the same input. For example, a pure function that calculates the distance between two points on a map will always give the same answer for the same pair of points, no matter what time of day, weather, or traffic conditions are.

  • Do not have any side effects. For example, a pure function that converts miles to kilometers will not change the value of the original miles variable, nor will it print anything to the screen or save anything to a file.

  • Make the code easier to debug, test, and parallelize. For example, a pure function that finds the shortest path between two locations on a map can be easily tested by comparing its output with the expected output, and it can be run in parallel.

Higher-Order Functions

  • Accept one or more functions as arguments or return a function. For example, a higher-order function takes a function and a list of locations as arguments and applies the function to each location. Just like a GPS device that takes a route preference (such as fastest, shortest) and a list of destinations as inputs and calculates the best route for each destination according to the preference.

  • Enable abstraction and composition of functions. For example, a higher-order function that takes two functions and returns a new function that combines them. It's like a GPS device that takes two route preferences (such as fastest and scenic) and returns a new preference.

  • Provide common patterns such as map, filter, and reduce. For example, a GPS device takes filter criteria (such as distance, rating, or category) and a list of nearby attractions as inputs and returns a new list that matches the criteria.

Transformers :

  • Take a function and return a modified version of it. For example, a GPS device takes a route calculation function and returns a new function that remembers the routes it has already calculated and reuses them if possible.

  • Allow changing the behavior or output of a function without changing its source code For example, a GPS device that takes a route calculation function and returns a new function that records the start and end points of each route it calculates.

Hope this gives you a clear idea of functional programming concepts. Now we know how to use a functional style toolkit to solve any problem, why not just try one in both styles?

Let’s Solve a Problem Using Both Styles

Imagine you have a list of tweets (whatever X-blah-weets people are calling it now), and you want to count all the spam tweets. Let's dive into both styles and handle a common programming task.

Mental modal of Imperative style Problem-Solving (How and What Part)

  • Pass a list of tweets to a loop

  • Initialize a spam counter

  • For each iteration:

    • If a tweet is spam, increase the counter

    • else do nothing

  • End of the loop

  • Return spam counter.

public int getTotalSpamTweetsCount(List<Tweet> tweets) {
    int totalSpamTweets = 0 ;
    for(Tweet tweet : tweets) { 
    if(tweet.isSpam()) {
        totalSpamTweets++;
      } 
    } 
    return totalSpamTweets;
}

Don't worry if you don't understand the java code. This is just to give you an idea. The basic principle remains the same no matter what language you are using!!

Here’s an explanation of the Java code snippet :

  1. The method getTotalSpamTweetsCount takes in a list of Tweet objects as an argument.

  2. It initializes a counter-variable totalSpamTweets to 0.

  3. The method uses a for-each loop to iterate through each Tweet object in the list.

  4. Inside the loop, it checks if the current tweet is spam by calling the isSpam() method on the Tweet object.

  5. If the tweet is spam, then totalSpamTweets , the counter is incremented by 1.

  6. After the loop finishes, the method returns the value of totalSpamTweets, which represents the total number of spam tweets in the list.

Functional style Problem-Solving Mental modal (What Part)

  • Get the count of all the spam tweets or

  • Filter all the spam tweets and get the count.

int totalSpamTweets = tweets.stream().filter(Tweet::isSpam).count();

Here’s an explanation of the Java code snippet :

  1. The stream() method is called on the tweets list to create a stream of Tweet objects.

  2. The filter() method is used on the stream to filter out only those tweets that are spam. It uses the Tweet::isSpam method reference to check if a tweet is spam.

  3. The count() method is then called on the filtered stream, which counts the number of elements in it (which are the spam tweets) and returns it as a long.

  4. This count is then stored in the totalSpamTweets variable.

Adding New Feature: Count Spam Tweets Posted in the Last 24 hours

Now, let's say we want to count only the spam tweets that were posted in the last 24 hours. Let's see how to do it using the imperative style

int totalRecentSpamTweets = 0;
Instant twentyFourHoursAgo = Instant.now().minus(Duration.ofHours(24);

for (Tweet tweet : tweets) {
    if (tweet.isSpam() && tweet.getTimestamp().isAfter(twentyFourHoursAgo)) {
        totalRecentSpamTweets++;
    }
}

Here’s an explanation of the Java code snippet :

  1. The code initializes a counter-variable totalRecentSpamTweets to 0.

  2. It then gets the current time and subtracts 24 hours from it to get the time 24 hours ago, which is stored in the twentyFourHoursAgo variable.

  3. The code uses a for-each loop to iterate through each Tweet object in the tweets list.

  4. Inside the loop, it checks if the current tweet is spam by calling the isSpam() method on the Tweet object and having a condition to check if the tweet was posted in the last 24 hours.

  5. If both conditions are met, the totalRecentSpamTweets , the counter is incremented by 1.

  6. After the loop finishes, the value of totalRecentSpamTweets represents the total number of spam tweets in the list that were posted in the last 24 hours.

As you can see, to add one more condition here, we had to do a lot of rituals & ceremonies. If this method is being used by many callers then all callers would have been impacted.

Now let's see the power of functional style

long totalRecentSpamTweets = tweets.stream()
    .filter(Tweet::isSpam)
    .filter(tweet -> tweet.getTimestamp().isAfter(Instant.now().minus(Duration.ofHours(24))))
    .count();

Here’s an explanation of the Java code snippet :

  1. The stream() method is called on the tweets list to create a stream of Tweet objects.

  2. The filter() method is used twice on the stream to filter out only those tweets that are spam and were posted in the last 24 hours. The first filter() uses the Tweet::isSpam method reference to check if a tweet is spam, while the second filter() uses a lambda expression to check if the tweet’s timestamp is after the time 24 hours ago.

  3. The count() method is then called on the filtered stream, which counts the number of elements in it (which are the recent spam tweets) and returns it as a long.

  4. This count is then stored in the totalRecentSpamTweets variable.

As you can see, we have just added a condition saying to filter all tweets within the last 24 hours. This is more concise and readable than the imperative style. It makes it so easy to add new requirements or any possible changes in the future without heavy lifting.

Adding New Feature: Total Spam Tweets > 10 Retweets

Let’s say we want to add a feature to only count tweets that have more than 10 retweets. Here’s how we can do it in both styles:

long totalRecentPopularSpamTweets = 0;
for (Tweet tweet : tweets) {
    if (tweet.isSpam() && tweet.getRetweetCount() > 10
        && tweet.getTimestamp().isAfter(Instant.now().minus(Duration.ofHours(24))))
        totalRecentPopularSpamTweets++;
    }
}

In functional style, it's much easier

long totalRecentPopularSpamTweets = tweets.stream()
    .filter(Tweet::isSpam)
    .filter(tweet -> tweet.getTimestamp().isAfterInstant.now().minus(Duration.ofHours(24))))
    .filter(tweet -> tweet.getRetweetCount() > 10)
    .count();

As you can see, we just had to add a simple condition of retweet count > 10. By now you would have got clear idea about which approach is cleaner, easier, and more precise. We just have to add the required condition or in simple terms, we have just to tell what to do in a functional style and avoid step-by-step rituals.

Rewire Yourself with Functional Style

If you are still reading, I am glad you managed to survive this trip of analogies and my poor jokes!! I hope you haven't been overwhelmed by the amount of code snippets I have shared with explanations. If you are coming from an imperative style of programming, it can take some time to get used to the functional style. In imperative style, you are habitual in solving a problem in a series of steps, whereas in functional style you have to rewire your mind. Functional style of programming is a powerful tool that can help you write more concise, readable, and maintainable code. It’s particularly well-suited for solving problems that involve data processing and manipulation, such as filtering, mapping, and reducing collections of data. Another benefit of the functional style is its ability to abstract away low-level details and focus on the “what” part rather than the “how” part of a problem. This can make your code more readable and easier to understand, especially for complex problems.

Conclusion

In this blog post, we’ve only scratched the surface of what’s is functional programming and how to change your way of thinking if you are trying to solve a problem using a functional style. Trust me, it's so much fun if you start thinking functionally. There are many more concepts to explore, such as immutability, pure functions, and transformers. These concepts can help you write even more robust and efficient code. If I managed to introduce you to functional programming through my unconventional analogies, then perhaps I will try to think of some more crazy way to explain functional stuff in detail and put it in a blog, hope you have enjoyed the trip, and I will leave you alone and let you to be Sherlock 🕵️‍♂️.