loading...
Cover image for Write Clean Code Without Loops

Write Clean Code Without Loops

shanif profile image Shani Fedida ・6 min read

Loops are messy - Photo by Daniel Fazio on Unsplash

A few years ago, I participated in a public workshop organized by Wix. We practiced TDD by implementing the Game Of Life kata with different constraints. One constraint was to write code without loops. I was shocked and stuck, I could not grasp how it is possible. Eventually, I figured it out: map, filter, and reduce! I knew this concept before but it wasn’t part of my daily programming (back then I wrote in C++). When I returned to C#, I started using this concept more and more and understood its benefits.

In this post, I show how to write clean code by replacing foreach loops with map, filter, and reduce. This approach is relevant to all languages.

TL;DR

  • Declarative code written with map, filter, and reduce is more readable and concise than imperative code written with foreach loops.
  • Reduce boilerplate code and avoid stupid bugs.
  • Achieve clean code by separating between the logic of which items to operate on and the operation itself.
  • Code is better in terms of the Single Responsibility Principle

What Are Map, Filter & Reduce?

Map, Filter and Reduce are functions that work on collections. They receive a collection and a function and return a new collection based on the result of the function.

  • Map — maps each object in a collection into a new object. It returns a new collection with the same size as the given collection.
  • Filter — filters a collection according to a given condition. The new collection includes only the elements that pass the condition.
  • Reduce — reduces a collection down to a single value.

I wrote the code examples with C# LINQ and simplified them for the post. In real-life systems the code is not clearly divided into functions, instead, most of the code is inside the loop.

Replace foreach with map

Sometimes we see code with this structure:

Now, let’s rewrite it using LINQ Select which is the LINQ version of map :

This code is shorter — a single line!

Replace if with filter

Sometimes we see code with these structures:

The loops contain two logics:

  1. Which items to operate on.
  2. The operation itself.

We can replace if with LINQ Where, which is the LINQ version of filter. When we replace if (someCondition) continue; with Where we write the negative form of the condition.


The two logics are now separated. Each logic has a single responsibility.

Replace Boilerplate Code with Any

Sometimes we see code with these structures:

These are patterns for “return true if at least one of the items returns true for a given condition” aka the Any function. LINQ Any determines whether any element of a sequence exists or satisfies a condition. Any is a specific form of a reduce function.

Even though we are used to writing these boilerplate patterns, there is always a chance to cause bugs by doing a wrong initialization or confusing between the AND operator and the OR operator.

Let’s rewrite the code by using LINQ Any:

This code is clearer, shorter (single line!), and emphasizes the code’s meaning. It is also more efficient, Any stops as soon as the result can be determined.

Replace Boilerplate Code with All

Sometimes we see code with these structures:

These patterns mean “return true if ALL the items return true for a given condition” aka the All function. LINQ All Determines whether all elements of a sequence satisfy a condition. All is a specific form of a reduce function.

Similar to Any, there is a chance to miswrite this boilerplate code and to cause bugs.

Let’s rewrite the code by using LINQ All.

This code is clearer, shorter (single line!), and emphasizes the code’s meaning.


Code Evolution

In this section, I show how writing code with foreach loops can lead to spaghetti code while writing code with map, filter, and reduce results in clear, readable, and maintainable code.

Let’s imagine we are building a fun system for organizing fun days. 💃 🎉 🍻
Each employee should receive a welcome email for the fun day they registered to. Here is the code:

Now we discover that some people forgot to register. They were unaware of the fun days 😲. What should we do? Let’s send them a reminder email to register for a fun day.

Sending a reminder also operates only on current employees. The current code already finds the fun day for each employee, so this functionality is quite simple to add.

Great! Now people will not miss out on the fun. 😃

Now we discover that some people registered for a fun day but forgot to register for the optional activities. Let’s send them a reminder to register for the optional activities.

The current code already finds the fun day for each employee, so let’s use it to add the new functionality.

Yammi 😋 we got spaghetti… code! 🤦‍♀

Computer vector created by freepik — www.freepik.com

The problems in the above code are:

  • Many logics mixed together aka violates the Single Responsibility Principle.
  • In each row, employee passed different filters so it is hard to grasp which filters it passed.
  • Quite nested code (4 nesting levels)

Let’s Try Again — Rewrite 💣💣💣

Now we start from scratch, writing the code with LINQ.

Let’s rewrite the first feature —  welcome email.

The original code:


Rewrite with LINQ:

What are the differences?

  • In both implementations SendWelcomeEmail operates only on employes who are registered for a fun day. In the LINQ implementation, I separated the logic of whom to operates and the operation itself.

  • I separated the filtering of current employees from the employes' loop. The filtering is done in a separate function called GetCurrentEmployees which emphasizes the code's meaning. I replaced if (IsPastEmployee(employee)) continue; and if (IsFutureEmployee(employee)) continue; with Where.

  • Instead of searching the fun day inside the employes' loop, I created a dictionary between an employee and a fun day called employee2Funday. I replaced if (funday.IsEmployeeRegistered(employee.Id)) with employee2Funday.Where.

  • We assume that an employee is registered to a single fun day. The original code implicitly chooses the first fun day by using the break statement. The new implementation uses LINQ FirstOrDefault which is more explicit. FirstOrDefault is a specific form of a reduce function.


Now let's implement the next feature — a reminder email to register for a fun day for employees who didn’t register for any fun day.

The original code:

Rewrite with LINQ:

What are the differences?

  • I separated the logic of whom to operates and the operation itself. The function SendUnregisteredToFunday operates only on employes who are unregistered for any fun day.

  • In the original code, the two functionalities are tangled together. In the rewritten code each functionality has its own method. This is possible because the data this functionality operates on, aka the employees and whether they are registered to a fun day, exists outside of the first functionality implementation.


Now let's implement the last feature — a reminder email for registering to the optional activities.

The original code:

Rewrite with LINQ:

What are the differences?

  • Again, I separated between the logic of whom to operates and the operation itself. The function SendEmailAboutOptionalActivities operates only on employees who are registered for a fun day but unregistered to any optional activity.

  • In the original code, the functionalities are tangled together. In the rewritten code each functionality has its own method. This is possible because the data this functionality operates on, aka the employees and whether they are registered to a fun day, exists outside of the other functionalities implementation.

  • IsUnregisteredToOptionalActivities is implemented with Where and Any instead of boilerplate code. It is shorter and clearer.


This paradigm is part of the functional programming paradigm which has some concepts that help write clean, readable, and less error-prone code. Some of these concepts are immutable classes, avoid variables reassignment, and avoid shared state.

I currently write code in Scala and I barely see foreach loops. Out of curiosity, I gathered some statistics from the codebase I work on: map has ~3500 occurrences, filter has ~3000 occurrences, and foreach has only ~500 occurrences.

Discussion

pic
Editor guide