The original:
Writing flat & declarative code


The author:
Peeke Kuepers

Add a little functional programming to your code

Recently I have become very interested in functional programming. I’m fascinated by the concept of applying mathematics to enhance abstraction and forced purity to avoid side effects and achieve good reusability of code. Also, functional programming is very complex.

Functional programming has a very steep learning curve because it comes from category theory in mathematics. Soon after contact, terms such as composition, identity, functor, monad, and contravariant will be encountered. I don’t know much about these concepts, which is probably why I’ve never used functional programming in practice.

I began to wonder: Could there be some middle form between regular imperative programming and fully functional programming? It allows you to introduce some of the nice features of functional programming into your code base while temporarily preserving the old code that you already have.

For me, the biggest thing functional programming does is force you to write declarative code: code that describes what you do, not how you do it. This makes it easy to understand what a particular block of code does without really knowing how it works. Writing declarative code, it turns out, is one of the easiest parts of functional programming.

cycle

. A loop is an imperative control structure that is difficult to reuse and insert into other operations. In addition, it had to constantly change the code in response to new iteration requirements.


— Luis Atencio

So let’s take a look at loops, which are a good example of imperative programming. Loops involve a lot of syntax that describes how their behavior works, not what they’re doing. For example, look at this code:

function helloworld(arr) {
    for (let i = 1; i < arr.length; i++) {
        arr[i] * = 2
        if (arr[i] % 2 = = = 0) {
            doSomething(arr[i])
        }
    }
}
Copy the code

What is this code doing? It multiplies all the numbers in the array except the first number (let I = 1) by 2 and does something if they are even (if (arr % 2 === 0)). In the process, the value of the original array is changed. But this is usually not necessary because the array might be used elsewhere in the code base, so the change you just made could lead to unexpected results.

But the main reason is that the code is hard to read at a glance. It is imperative, and the for loop tells us how to iterate over a list of numbers, in which a function is conditionally called using an if statement.

We can rewrite this code declaratively by using the array method. Array methods express what is done directly. Common methods include forEach, Map, filter, reduce, and slice.

The result would look something like this:

function helloworld(arr) {
    const evenNumbers = n = > n % 2 = = = 0

    arr
        .slice(1)
        .map(v = > v * 2)
        .filter(evenNumbers)
        .forEach(v = > doSomething(v))    
}
Copy the code

In this example, we used a nice, flat chain structure to describe what we were doing, to make the intent clear. In addition, we avoid changing the original array to avoid unwanted side effects, since most array methods return a new array. When arrow functions start to get more complex, you can extract them into a specific function, such as evenNumbers, to keep the structure as simple and readable as possible.

In the example above, the chained call does not return a value, but ends with forEach. However, we can easily peel off the last part and return the result so that we can process it elsewhere. If you need to return anything other than a divisor, you can use the reduce function.

For the next example, suppose we have a set of JSON data that contains points scored by different countries in a fictional singing contest:

[
    {
        "country": "NL".
        "points": 12
    },
    {
        "country": "BE".
        "points": 3
    },
    {
        "country": "NL".
        "points": 0
    },
    .
]
Copy the code

We wanted to calculate the total score scored by The Netherlands (NL), which we can assume is a very high score based on the impression of its strong musical ability, but we wanted to confirm this more precisely.

Using loops might look like this:

function countVotes(votes) {
    let score = 0;

    for (let i = 0; i < votes.length; i++) {
        if (votes[i].country = = = 'NL') {
            score + = votes[i].points;
        }
    }

    return score;
}
Copy the code

Using the array refactoring, we get a cleaner code snippet:

function countVotes(votes) {
    const sum = (a. b) = > a + b;

    return votes
        .filter(vote = > vote.country = = = 'NL')
        .map(vote = > vote.points)
        .reduce(sum);
}
Copy the code

Sometimes reduce can be a little hard to read, but it helps to extract the Reduce function. In the code snippet above, we defined a sum function to describe what the function does, so the method chain remains readable.

If the else statement

Now, let’s talk about our favorite if else statement, which is another good example of imperative code. To make our code more declarative, we’ll use ternary expressions.

A ternary expression is an alternative syntax for an if else statement. The following two code blocks have the same effect:

// Block 1
if (condition) {
    doThis(a);
} else {
    doThat(a);
}

// Block 2
const value = condition ? doThis(a) : doThat(a);
Copy the code

Ternary expressions are useful when defining (or returning) a constant. Using an if else statement limits the scope of the variable to the statement. We can avoid this problem by using the ternary statement:

if (condition) {
    const a = 'foo';
} else {
    const a = 'bar';
}

const b = condition ? 'foo' : 'bar';

console.log(a); // Uncaught ReferenceError: a is not defined
console.log(b); // 'bar'
Copy the code

Now, let’s see how this can be applied to refactor some of the more important code:

const box = element.getBoundingClientRect(a);

if (box.top - document.body.scrollTop > 0 && box.bottom - document.body.scrollTop < window.innerHeight) {
    reveal(a);
} else {
    hide(a);
}
Copy the code

So what happens to the code above? The if statement checks whether the element is currently in the visible part of the page, and this information is not expressed anywhere in the code. Based on this Boolean value, call reveal() or hide().

Converting this if statement to a ternary expression forces us to move the condition to its own variable. This allows us to combine ternary expressions on a single line, and it’s nice that the name of the variable now communicates the Boolean representation.

const box = element.getBoundingClientRect(a);
const isInViewport = 
    box.top - document.body.scrollTop > 0 && 
    box.bottom - document.body.scrollTop < window.innerHeight;

isInViewport ? reveal(a) : hide(a);
Copy the code

From this example, the benefits of refactoring may seem small. Here’s a more complicated example:

elements
    .forEach(element = > {
        const box = element.getBoundingClientRect(a);

        if (box.top - document.body.scrollTop > 0 && box.bottom - document.body.scrollTop < window.innerHeight) {
            reveal(a);
        } else {
            hide(a);
        }

    });
Copy the code

This is bad, breaking up our elegant flat call chain and making the code harder to read. We use the ternary operator again, and when we use it, we use isInViewport checks, separate from its own dynamic functions.

const isInViewport = element = > {
    const box = element.getBoundingClientRect(a);
    const topInViewport = box.top - document.body.scrollTop > 0;
    const bottomInViewport = box.bottom - document.body.scrollTop < window.innerHeight;
    return topInViewport && bottomInViewport;
};

elements
    .forEach(elem = > isInViewport(elem) ? reveal(a) : hide());
Copy the code

In addition, now that we have moved isInViewport into a separate function, we can easily put it inside its own helper class/object:

import { isInViewport } from 'helpers';

elements
    .forEach(elem = > isInViewport(elem) ? reveal(a) : hide());
Copy the code

Although the above example depends on dealing with arrays, this encoding style can be used without explicitly dealing with arrays.

For example, look at the following function, which verifies the validity of a password with three rules.

import { passwordRegex as requiredChars } from 'regexes'
import { getJson } from 'helpers'

const validatePassword = async value = > {
  if (value.length < 6) return false
  if (!requiredChars.test(value)) return false

  const forbidden = await getJson('/forbidden-passwords')
  if (forbidden.includes(value)) return false

  return value
}

validatePassword(someValue).then(persist)
Copy the code

If we wrap the initial value in an array, we can use all the array methods used in the above example. In addition, we have packaged the validation functions as validationRules to make them reusable.

import { minLength. matchesRegex. notBlacklisted } from 'validationRules'
import { passwordRegex as requiredChars } from 'regexes'
import { getJson } from 'helpers'

const validatePassword = async value = > {
  const result = Array.from(value)
    .filter(minLength(6))
    .filter(matchesRegex(requiredChars))
    .filter(await notBlacklisted('/forbidden-passwords'))
    .shift(a)

  if (result) return result
  throw new Error('something went wrong... ')
}

validatePassword(someValue).then(persist)
Copy the code

There is currently a proposal for pipe operators in JavaScript. Using this operator, you no longer need to replace the original value with an array. You can call the function after the pipe operator directly from the previous value, much like Array’s Map function. The modified code might look something like this:

import { minLength. matchesRegex. notBlacklisted } from 'validationRules'
import { passwordRegex as requiredChars } from 'regexes'
import { getJson } from 'helpers'

const validatePassword = async value = >
  value
    |> minLength(6)
    |> matchesRegex(requiredChars)
    |> await notBlacklisted('/forbidden-passwords')

try { someValue |> await validatePassword |> persist }
catch(e) {
  // handle specific error, thrown in validation rule
}
Copy the code

It’s important to note, though, that this is still a very early proposal, but wait a bit.

The event

Finally, let’s look at event handling. Event handling has always been difficult to code in a flat way. Promises can be made to maintain a chained, flat programming style, but promises can only be resolved once, and events must be fired multiple times.

In the example below, we create a class that retrieves each input value from the user, resulting in an autocomplete array. First check that the string is longer than the given threshold length. If the criteria are met, the auto-completion result is retrieved from the server and rendered as a series of labels.

Note that the code is not “pure” and uses the this keyword frequently. Almost every function calls this:

The author uses “this keyword” as a pun

import { apiCall } from 'helpers'

class AutoComplete {

  constructor (options) {

    this._endpoint = options.endpoint
    this._threshold = options.threshold
    this._inputElement = options.inputElement
    this._containerElement = options.list

    this._inputElement.addEventListener('input'. (a) = >
      this._onInput())

  }

  _onInput (a) {

    const value = this._inputElement.value

    if (value > this._options.threshold) {
      this._updateList(value)
    }

  }

  _updateList (value) {

    apiCall(this._endpoint. { value })
      .then(items = > this._render(items))
      .then(html = > this._containerElement = html)

  }

  _render (items) {

    let html = ' '

    items.forEach(item = > {
      html + = `<a href="The ${ item.href }">The ${ item.label }</a>`
    })

    return html

  }

}
Copy the code

We’ll rewrite this code in a better way by using Observable. An Observable can simply be thought of as a Promise that resolves multiple times.

The Observable type can be used for data sources based on the push model, such as DOM events, timers, and sockets

The Observable proposal is currently stage-1. The implementation of the Listen function below is copied directly from the Proposal on GitHub, converting event listeners into Observables. As you can see, we can rewrite the entire AutoComplete class as a chain of functions for a single method.

import { apiCall. listen } from 'helpers';
import { renderItems } from 'templates'; 

function AutoComplete ({ endpoint. threshold. input. container }) {

  listen(input. 'input')
    .map(e = > e.target.value)
    .filter(value = > value.length > = threshold)
    .forEach(value = > apiCall(endpoint. { value }))
    .then(items = > renderItems(items))
    .then(html = > container.innerHTML = html)

}
Copy the code

Since most Observable libraries are too big to implement, I’m looking forward to an ES native implementation. The Map, filter, and forEach methods are not yet part of the specification, but there are extensions to the API implementation in Zen-Observable, which is itself an implementation of ES Observables.

I hope you’ll be interested in these “flattening” patterns. Personally, I enjoy rewriting my programs this way. Every piece of code you touch is easier to read. The more experience you gain with this technique, the more you realize this. Remember this simple rule:

The flatter the better!