9

Peeke.nl

 6 years ago
source link: https://peeke.nl/writing-flat-code
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Writing flat & declarative code

:

Bringing a bit of functional programming to your codebase

Recently I have picked up an interest in functional programming. Its concepts fascinate me: applying math to enable strong abstractions and forcing purity to avoid side effects and enable good reusability of code. I also find it incredibly complex.

profile.jpg

Peeke Kuepers https://twitter.com/peeke__/status/920930799121203200 (Re)published on 6-6-2017 20:36

Functional programming can have an incredibly steep learning curve. With it's roots in mathematical category theory it doesn't take long before you encounter terms like composition, identities, functors, monads, contravariants and so on. I'm not even close to understanding all these concepts and maybe that was why I never put functional programming into practice. I’m sure many of us can relate.

It got me thinking: might there be some intermediate form between regular — imperative — code and full-on functional code? A form allowing us to apply some functional goodness to our codebase, whilst leaving the complex stuff out for the time being?

The best feature of functional programming to me,  is that it forces you to write declarative code: code that describes what you do, as opposed to describing how stuff works. This means that you can easily understand what a specific block of code does, without understanding exactly how it works. As it turns out, writing your code declaratively is one of the easiest parts of functional programming!

Loops

…a loop is an imperative control structure that’s hard to reuse and difficult to plug in to other operations. In addition, it implies code that’s constantly changing or mutating in response to new iterations.

Luis Atencio

So let’s start by taking a closer look at loops. Loops are a great example of imperative code (the opposite of declarative code). Loops involve a lot of syntax describing how their behaviour works instead of what they do. For example, look at this piece of code:

function helloworld(arr) {

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

}

What does this construction do? It takes all the numbers except the first (let i = 1), multiplies them by 2 and does something with the value if they are even (if (arr % 2 === 0)). In the process, the original array is mutated. This is generally unwanted  because the array may be used somewhere else in your codebase as well. The mutation you just made can lead to unexpected results over there. 

The main complaint though, is that this code is difficult to comprehend at first sight. It’s written imperatively. The for loop tells us how we loop over the array. Inside, an if statement is used to conditionally invoke a function.

We can easily rewrite this piece of code in a declarative way by using array methods. Array methods directly convey what they do. Well-known array functions include: forEach, map, filter, reduce and slice (there are a handful of extra functions, especially considering ES6 and 7, but these are the most used ones). 

The result:

function helloworld(arr) {

const evenNumbers = n => n % 2 === 0

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

}

In this example we have a nice, flat chain of methods describing what we are doing. A clear signaling of intent. Also we’ve avoided mutating the original array preventing unwanted side effects, since most array functions return a new array. When the arrow functions within start to grow more complex you can easily extract them into a dedicated function — like evenNumbers — , keeping the chain simple and readable.

In this case, we’re not returning anything as indicated by the chain ending with a forEach method. We could just as easily strip this last part off and return the result so we can process it somewhere else. If you need to return anything other than an array, you can use the reduce function. 

For our next example, let’s say we have a set of JSON data containing the points countries received in some fictional singing competition:

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

We want to count the total number of points the Netherlands (NL) have received. Judging by our great musical capabilities we can assume it’s a very high amount, but we would like to be exact.

Using loops one might come up with something 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;

}

Refactoring this using array functions, we end up with a much cleaner piece of code:

function countVotes(votes) {

const sum = (a, b) => a + b;

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

}

Reduce can be a bit difficult to read at times. It can help to extract the reducing function. In the snippet above we’ve defined a sum method to describe what the function does, so the flat method chain remains readable.

If else statements

Next, let’s take a swing at our beloved if else statements. If else statements are another great example of imperative code. To make our code more descriptive, we are going to use ternary statements. 

A ternary statement is an alternative syntax to the if else statement. Both blocks of code below have the same effect:

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

// Block 2
const value = condition ? doThis() : doThat();

Ternary statements are exceptionally useful when defining variables (or returning values) with the conditional value as a const. Using an if else statement would confine the use of the variable to the scope of the statement. By using a ternary statement we avoid this problem:

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'

Now let’s have a look at how we might apply this to refactor some more imperative code:

const box = element.getBoundingClientRect();

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

So what’s happening in the code above? The if statement checks whether an element is currently within the visible portion of the page. This information isn’t communicated anywhere however. Based upon this boolean value, either reveal() or hide() is invoked. 

Converting this if statement to a ternary one forces us to move the condition to it’s own variable. This way we can fit the ternary on a single line and as a welcome side effect we now communicate what the boolean value represents through the name of the variable. Nice!

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

isInViewport ? reveal() : hide();

Given only this example, the benefits of this refactoring may seem small. But let’s extend the example to span multiple elements:

elements
.forEach(element => {

const box = element.getBoundingClientRect();

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

});

That’s not good. We broke our nice, flat chain and consequently made our code harder to read. Let’s bring in the ternary operator again. And while we’re at it, lets separate the isInViewport check to it’s own dynamic function.

const isInViewport = element => {
const box = element.getBoundingClientRect();
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() : hide());

Also, now that we moved isInViewport to it's own function we can easily tuck it away inside it’s own helper class/object:

import { isInViewport } from 'helpers';

elements
.forEach(elem => isInViewport(elem) ? reveal() : hide());

While the example above relies on the fact that you’re dealing with an array of elements, you can also apply this style of coding when not explicitly dealing with arrays. 

For example, have a look at the following function. It validates a password against a set of 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 = getJson('/forbidden-passwords')
if (forbidden.includes(value)) return false

return value

}

validatePassword(someValue).then(persist)

If we wrap our initial value with an array, we are able to use all the array methods we were using in the examples above. Also we've tucked the validation functions in 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.of(value)
.filter(minLength(6))
.filter(matchesRegex(requiredChars))
.filter(await notBlacklisted('/forbidden-passwords'))
.shift()

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

}

validatePassword(someValue).then(persist)

There's currently a proposal for a pipe operator in javaScript. Using this operator, there's no need to wrap our value with an array anymore. The function after the pipe operator simply gets called with the preceding value. A bit like the Array's map function. Given the right modifications the code could 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
}

Keep in mind this is still a very early proposal. Very exciting though!

Events

Finally, let’s take a look at event handling. Event handling is traditionally difficult to write in a flat fashion. With regular callback driven functions you could promisify them to enable a chained, flat style of programming. Promises only resolve once though, and our event is definitely going to fire multiple times.

In the example below we created a class which retrieves an array of autocomplete values upon each input of the user. We first check if the string is longer that a given threshold length. If so, we retrieve the autocomplete results from the server and render them as a sequence of tags.

Besides the indirection in the code, notice the impurity. There is a frequent use of the this keyword. Nearly every function accesses this keyword (pun intended): 

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', () =>
this._onInput())

}

_onInput () {

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="${ item.href }">${ item.label }</a>`
})

return html

}

}

There’s a pretty good way to rewrite this by using a concept called the Observable. Think of an Observable as a Promise that can resolve multiple times.

The Observable type can be used to model push-based data sources such as DOM events, timer intervals, and sockets

The Observable is currently a stage 1 proposal. The implementation of the listen function below is copied from the proposal over at GitHub. Basically it converts an event listener into an Observable. As you can see, we’re able to rewrite the entire AutoComplete class to a single method chain of functions.

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)

}

I’m really thrilled with Observables coming to ES itself, since most library implementations are way too large for my liking. The map, filter and forEach methods aren’t part of the spec yet, but are implemented in the extended API of the zen-observable implementation of ES Observables. Which happens to be really light as well! 

I hope I’ve managed to interest you in some of these ‘flat’ patterns. Personally, I’ve really enjoyed rewriting my programs this way. Every bit of code you touch instantly gets more readable. And the more experience you gain with this technique, the more situations you recognize it is applicable. Just remember this simple rule of thumb:

The flatter the better!

If you’ve got any questions or want to discuss further, please do! You can reach me in the comments or on Twitter @peeke__.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK