Real world functional programming in JS
Tips and guidelines for scalable and easily maintainable code bases!
Summary
Functional programming in JavaScript in 2017 is finally a DO. Most of the new tools for this environment are based on functional concepts and it seems like functional programming is becoming more and more popular due to its advantages. On Rung, functional programming is the main mindset for all projects and subprojects and it scales really well. We have been using functional programming deeply in the last 2 years, from JS up to Haskell. Bugs got less frequent and easier to track and fix, and, different from what you may think, our team got it really fast! Keeping the code standard and working with several people without worrying with ones breaking each other code is now a relief! The tips below are not empiric, they are being used daily by us to deliver high quality code.
Recommended libraries
Ramda
Ramda is like Lodash, but made right. It has a lot of functions to work with data and to work with composition. Unlike Lodash, in Ramda, functions come before data. This allows point-free programming (like Haskell).
Ramda Fantasy
Ramda Fantasy is a set of common monadic structures to work with values, errors, side-effects and state.
Folktale
Folktale is an alternative to Ramda Fantasy. It is better maintained and exports tools for working with concurrency, computations that may fail and modelling data using algebraic data types.
Do
Return everything
Everything should return, also functions that emit side-effects. Try to preserve homomorphism. Try to keep the return type of a function consistent. Don't mix computations that transform data with things like writing to screen. Modular code is easier to maintain. Use parameters for replaceable data instead of hard-coding.
Don't
let result = 1
for (let i = 2; i <= 5; i++) {
result *= i
}
console.log('Fact of 5: ', result)
Do
const fact = n => n === 0
? 1
: n * fact(n - 1)
console.log('Fact of 5: ', fact(5))
Tacit programming
Tacit programming is also known as point-free programming. It means, basically, using simple functions to compose more complexes functions (and omitting arguments). One of the functional programming pillars is composition.
Don't
console.log(
sum(map(x => x + 1, range(0, 100)))
)
Do
const transform = pipe(map(inc), sum))
console.log(transform(range(0, 100)))
Use modules
Isolate your logic inside modules that do one thing, and do that well. Modules should export functions and, when using a type checker, types.
Composition over inheritance
Inheritance is definitely not how you deal with data and behavior in functional programming. Computations are modeled using behaviors. Some languages call them type classes.
Memoize functions
Memoize pure functions that are used several times with the same input parameters. Ramda provides
a function called memoize
for that!
Do
const fact = memoize(n => 0 === n
? 1
: n * fact(n - 1))
fact(5) // Calculates fact for 5, 4, 3 ...
fact(5) // Instantaneous
fact(3) // Instantaneous
Avoid
Mutability
Mutability is evil! It can set your house on fire, kill your cat and buy costumes on e-bay using your credit card! Be careful!
Functional programming heavily relies on immutability. Redefining a value or the property of an
object is therefore forbidden. Don't use neither var
nor let
. Everything should be a const
.
Don't use functions like .push
or .splice
because they change the value of the original
parameter and are error-prone.
Don't
var approved = []
for (var i = 0; i < approved.length; i++) {
if (users[i].score >= 7) {
approved.push(approved)
}
}
Do
const approved = filter(user => user.score >= 7, users)
Bare code
Code outside a function is a side-effect inside a module. Effects should be explicit. Only static declarations are allowed.
Don't
user.js
const usersList = User.find().asArray()
usersList.forEach(user => {
console.log(user.name)
})
Do
user.js
const listUsers = () => User.find().asArray()
export const printNames = () => listUsers().forEach(user => {
console.log(user.name)
})
index.js
import { printNames } from './user'
printNames()
Loops
Native statement loops are forbidden. Loops are made to enforce side-effects and there is no
case of a loop where a side-effect wouldn't be expected. You don't need to use recursion most of
the time. There is a lot of functions and compositions that you can use to achieve your logic.
Ramda provides some functions like map
, filter
and reduce
that make loops completely
unnecessary.
Don't
const even = []
for (let i = 0; i <= 300; i++) {
if (i % 2 === 0) {
even.push(i)
}
}
console.log(even) // [0, 2, 4, 6, 8 ...]
Do
import { filter, range } from 'ramda'
const even = filter(n => n % 2 === 0)
console.log(even(range(0, 300))) // [0, 2, 4, 6, 8 ...]
Switch
In functional programming, imperative structures do not exist. switch
is intended to have
effects and known to have a complex flux. You can use the function cond
from Ramda instead.
cond
receives a list of pairs of functions where the first one is the predicate and the
second is the transformation.
Don't
const person = { name: 'Wesley' }
let result
switch (person.name) {
case 'Dayana':
result = person.name + ' is cool!'
break
case 'Wesley':
result = person.name + ' likes farting'
break
default:
result = 'Who is ' + person.name + '?'
}
console.log(result) // Wesley likes farting
Do
import { T, cond, propEq } from 'ramda'
const getDescription = cond([
[propEq('name', 'Dayana'), ({ name }) => `${name} is cool!`],
[propEq('name', 'Wesley'), ({ name }) => `${name} likes farting`],
[T, ({ name }) => `Who is ${name}?`]
])
console.log(getDescription({ name: 'Wesley' })) // Wesley likes farting
Try
Error handling shouldn't be handled by exceptions, but by either monads or promises.
Don't
try {
undefined.property
} catch (err) {
console.log(err)
}
Do
import { tryCatch } from 'ramda'
import { Either } from 'ramda-fantasy'
const computation = tryCatch(
() => undefined.property,
Either.Right,
Either.Left
)
console.log(computation()) // Left<TypeError>
Undefined and null
Here lies the root of undefined is not a function. Missing values lead to over-engineering,
lots of verifications and conditions and errors that break your application and cannot
be caught in compile-time. You should replace them by monads, like the Maybe
monad.
Don't
const safeDiv = (a, b) => {
if (b === 0) {
return undefined
}
return a / b
}
console.log(safeDiv(20, 0) + 10) // Ops
Do
import { Maybe } from 'ramda-fantasy'
const safeDiv = (a, b) => 0 === b
? Maybe.Nothing
: Maybe.Just(a / b)
safeDiv(20, 0).chain(result => {
console.log(result + 10) // Never falls here
})
Classes
In general, using classes enforce effects and directly mutability. You can replace them by literal objects and functions that work on these objects.
Don't
class Person {
setName(name) { this.name = name }
getName() { return this.name }
}
let person = new Person()
person.setName('Cassandra')
Do
import { lensProp, prop, set } from 'ramda'
const setName = set(lensProp('name'))
const getName = prop('name')
const person = setName('Cassandra', {})
Callbacks
Callbacks can guide you easily to Hadouken code. Promises or futures are the way to go here. If you are using a library that uses callbacks, you can promisify the function to transform the callback to a promise.
Don't
$.ajax('http://api.trello.com/me', me => {
$.ajax(`http://api.trello.com/tasks/${me.id}`, tasks => {
var finished = []
for (var i = 0; i < tasks.length; i++) {
if (tasks[i].done) {
finished.push(tasks[i])
}
}
})
})
Do
fetch('http://api.trello.com/me')
.then(({ id }) => fetch(`http://api.trello.com/tasks/${id}`))
.filter(prop('done'))
.tap(console.log)
Prototype extension
Doing something like String.prototype.x =
is like a virus! It spreads all over your code and
infects every piece, possibly causing unexpected behavior. Prefer isolating these behaviors in
functions.
Advantages
Optimization
Pure functions are easier to optimize. They can cached with their parameters, as long as having the same input will always generate the same output.
Testing
TDD is the way to go here. Pure functions are very easier to test and have no dependencies in general. Having a 100% coverage on a functional code base is a lot easier than in an imperative one.
Scalability
Functional code is easier to scale. Having, for example, a stateless server will allow you to have multiple instances of it in different machines without worrying with shared and dependent data. Running on clusters and multiple processors is possible and V8 can optimize to run pure computations on different processes.