How to Write Clean Code (Part 1)

How to Write Clean Code (Part 1)

Write Clean Code with Functional Programming's Wisdom of Experience

ยท

7 min read

Featured on Hashnode

Clean Code with Functional Programming

There are thousands of ways to write codes that achieve the same goal: Developers have different wisdoms, experiences and of course individual preferences.

But developers don't work alone.

Multiple studies show that developers spend much more time to read code than the time spent on thinking and actual coding.

How do we make sure developers understand each other's code quickly? We write clean code.

I personally recommend Clean Code by Robert Martin for general techniques to write clean code.

Some good old Functional Programming wisdom of experience can help us write clean code, too!

Functional Programming and Declarative Programming

Functional Programming (FP) is Declarative Programming.

While Imperative Programming spends lines of code describing the specific steps (flow control), How to do things, used to achieve the desired results, Declarative Programming abstracts the flow control process, and instead spend lines of code describing What to Do.

Talk is cheap. Show me the code.

Let's say we are going to write a function to cook a fried rice dish. You may write the code like this in a step-by-step style (Imperative Programming):

function cookFriedRice() {
    standUp();
    walkToKitchenSink();
    washHands();
    dryHands();
    walkToCupboard();
    openCubboard();
    grabAContainer();
    locateRiceInCubboard();
    getRiceIntoContainer();
    closeCupboard();
    walkToSink();
    rinseRice();
    walkToRiceCooker();
    cookRiceUsingRiceCooker();
    walkToFridge();
    grabAnEggFromFridge();
    walkToCupboard();
    grabAContainer();
    placeEggIntoContainer();
    beatEgg();
    waitUntilRiceIsCooked();
    // It's very nice of you to read this long to this line. 
    // As you can imagine, things go on
    // and on and on
}

What's the problem with Imperative Programming?

It's actually quite natural to start coding in Imperative Programming paradigm: listing out every step required to solve a problem. Get our hands dirty and dive right in!

But after one month, there is something wrong with the code (like fatal error) or you are tasked to extend the function so that it produces fried rice with egg and bacon.

First step, you need to (re)visit the code. Now you don't remember what you have done, or you have no idea because other developer(s) wrote or enhanced it before you.

You start from the first line and after reading the first 5 lines (in reality it could be more), you still don't know why these 5 lines are related to frying rice.

    standUp();
    walkToKitchenSink();
    washHands();
    dryHands();
    walkToCupboard();
    openCubboard();

The time to read those 5 lines is precious for any develop who wants to quickly understand the function.

Solution: Declarative Programming

How do we make the code cleaner so that developers can cooperate more efficiently?

We need to abstract the original code a bit more.

Instead of listing every single step to show how to fry rice, we describe what we do to fry rice. The Declarative Programming paradigm:

function cookFriedRice() {
    $rice = getRice();
    $egg = getEgg();
    return fry($rice, $egg);
}

Practical Example

We have the following Imperative Programming code:

function practicalExample($input1, $input2)
{
    // Loop #1
    foreach ($input1 as $k => $v) {
        // 1000 lines of code
    }

    // Loop #2
    foreach ($input2 as $k => $v) {
        // 2000 lines of code
    }

    // Loop #3
    foreach ($someArray as $k => $v) {
        // 1000 lines of code
        // 1
        // 2
        // ...
        // At line #500 and it throws an error
        $loop3CalculatedField1 = calculate($v, $a);
        // ...
        // 999
        $return .= $loop3CalculatedField1;
    }

    return $return;
}

After the first glance of the function, we notice that the function returns an array $return and 3 loops run. And it looks like Loop #1 and #2 prepare some variables which Loop #3 needs.

An error occurs at Line #500 in Loop #3, and you are assigned to solve the issue.

Let's assume it only takes one minute to discover there's no problem in the calculate() function. The problem is $a is invalid. Now we need to find out why and where $a is wrong.

What's the problem?

It takes a lot of time to identify $a because it can be defined/set anywhere in the 1000 + 2000 = 3000 lines of code above Loop #3. After a few hours of reading code, we realize the following:

function practicalExample($input1, $input2)
{
    // Loop #1
    foreach ($input1 as $k => $v) {
        // 1000 lines of code
        // 1
        // 2
        // ...
        // So deep inside Loop #1, there's a variable computed and you can't identify this line by the first glance
        $loop1CalculatedField1 = array('...');
        // ...
        $loop1CalculatedField2 = array('...');
        // ...
        // 1000
    }

    // Loop #2
    foreach ($input2 as $k => $v) {
        // 2000 lines of code
        // 1
        // 2
        // ...
        // So deep inside Loop #2, there's a variable computed, and you can't identify this line by the first glance
        $loop2CalculatedField1 = '';
        // ...
        $loop2CalculatedField2 = $loop1CalculatedField1;
        // ...
        // So deep inside.. and it modifies a variable defined in Loop #1 as well
        $loop1CalculatedField2[] = $loop2CalculatedField1;
        // ...
        // 2000
    }

    // Loop #3
    foreach ($loop1CalculatedField1 as $k => $v) {
        // 1000 lines of code
        // 1
        // 2
        // ...
        $loop3CalculatedField1 = calculate($v, $loop1CalculatedField2);
        // ...
        // 999
        $return .= $loop3CalculatedField1;
    }

    return $return;
}

The problematic $a is first defined in Loop #1 ($loop1CalculatedField2), and it's later modified in Loop #2.

Solution

How do we write cleaner code?

First of all, try to avoid putting too much comment and let the code speak for itself. Also, most of the time you need to tell your brain: Comment is not always right because comment might not be updated but code does.

What we really need to do is to describe the result. What do the Loop #1 and #2 give us?

The new Loop #1

list('a' => $loop1Calculated2, 'b' => $loop1Calculated1) = array_reduce(
    $input1,
    function ($carry, $v) {
        // 1000 lines of code
        // 1
        // 2
        // ...
        // Define/set other variables and those variables will stay as local scope (this anonymous function)
        $carry['b'] = array('...');
        // ...
        $carry['a'] = array('...');
        // ...
        // 1000
    }
);

The result of Loop #1 is 2 variables with no other side effects. The first 3 lines of code tell us what the following code block does:

  1. list('a' => $loop1Calculated2, 'b' => $loop1Calculated1) = ... tells us the result is 2 new variables are created on the practicalExample function scope. And the corresponding values are what the code block returns $a and $b
  2. array_reduce($input1, ... tells us we are looping array $input1
  3. function ($carry, $v) { ... } tells us the code block does not require any other input

That's all those 1000 lines of code do. I don't need to know how, I just need to know the result of it.

By reading the first 3 lines of summary, I understand the whole picture on an abstract level.

Compared to the original code, I have to read through the whole Loop #1 in order to see the whole picture on an abstract level.

The new Loop #2

list('a' => $loop2Calculated1, 'b' => $loop2Calculated2) = array_reduce(
    $input2,
    function ($carry, $v) use (&$loop1Calculated2, $loop1Calculated1) {
        // 2000 lines of code
        // 1
        // 2
        // ...
        $carry['a'] = '...';
        // ...
        $carry['b'] = $loop1Calculated1;
        // ...
        $loop1Calculated2[] = $carry['a'];
        // ...
        // 2000
    }
);

The result of Loop #2 is:

  • list('a' => $loop2Calculated1, 'b' => $loop2Calculated2) = ... tells us 2 variables are created: $loop2Calculated1 and $loop2Calculated2
  • array_reduce($input2, ...) tells us we are looping $input2
  • function ($carry, $v) use (&$loop1Calculated2, $loop1Calculated1) { ... tells us the code block needs 2 more inputs $loop1Calculated1 and $loop1Calculated2 and there's side effect: may modify $loop1Calculated2

The new Loop #3

return array_reduce(
    $loop1Calculated1,
    function ($carry, $v) use ($loop1Calculated1, $loop1Calculated2, $loop2Calculated1, $loop2Calculated2) {
        // 1000 lines of code
        // 1
        // 2
        // ...
        $carry .= calculate($v, $loop1Calculated2);
        // ...
        // 1000
    },
    'initial return string'
);

The result of Loop #3 is:

  • return array_reduce(..., function () {...}, 'initial return string') tells us the function returns a string
  • array_reduce($loop1Calculated1, ...) tells us we are looping $loop1Calculated1
  • function ($carry, $v) use ($loop1Calculated1, $loop1Calculated2, $loop2Calculated1, $loop2Calculated2) { ... tells us the code block needs 4 input variables and there's no side effect

The collapsed version of the new function

function paracticalExmaple()
{
    list('a' => $loop1Calculated2, 'b' => $loop1Calculated2) = array_reduce(...);
    list('a' => $loop2Calculated1, 'b' => $loop2Calculated2) = array_reduce(...);
    return array_reduce(...);
}

After the code is refactored to Declarative Programming paradigm/style, it will help us to:

  • Quickly understand What has been done without knowing how things are done
    • Although the total lines of code is actually increased, by looking at the first part of the code blocks (loops), you will clearly see the results, side effects and the inputs required to run that code block
    • Back on the debugging $loop1Calculated2 ($originally $a) topic, we can easily tell that it is involved in 2 different code blocks (loops) without much scrolling
  • Group codes in individual code blocks
    • Individual Scope
      • Variables defined in this scope does not affect the parent scope
    • It's like giving a summary line for each large code block. And multiple summary lines form a table of contents

Summary

Hope you enjoy this article. Next time I will talk about other techniques to write clean code. See you later!

Did you find this article valuable?

Support Li Li by becoming a sponsor. Any amount is appreciated!

ย