How to FP

Pure functionsMore FP concepts

Home»Top-down»More FP concepts

(This follows from Pure functions.)

There's much more to functional programming. We haven't really begun to see the power it brings.

Higher-order functions aid reusability

JavaScript has first class functions, which allow us to treat functions as datathat means that we can assign them to variables, pass them as arguments to other functions, return them from functions, etc.

A higher-order function is a function that takes one or more functions as parameters, returns a function, or both.

Functional programming tends to reuse a common set of functional utilities to process data.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const add = (x, y) => x + y
const subtract = (x, y) => x - y
const evaluate = (f, x, y) => f(x, y)

// evaluate is a function promoting reuse
console.log(`evaluate(add, 3, 2) yields ${evaluate(add, 3, 2)}`)
console.log(`evaluate(subtract, 3, 2) yields ${evaluate(subtract, 3, 2)}`)

// A real world vanilla JS example
// Array.sort takes a comparator function that takes 2 parameters and returns:
//   * a negative number if the second goes before the first
//   * zero if they are the same
//   * a positive number if the first goes before the second
console.log('Ascending sort', [3, 7, 1, 9, 4].sort((a, b) => a - b))
console.log('Descending sort', [3, 7, 1, 9, 4].sort((a, b) => b - a))

// We can do more than that: how about sorting strings by length
// rather than alphabetically?
console.log(
  'By length',
  ['red', 'green', 'blue'].sort((a, b) => a.length - b.length)
)

Object-oriented programming binds functions and data together, so that the functionsnow called "methods"can only operate on the data to which they are attached. For example, in the Array.sort method above, we must start with an array and then do the sort on that array.

Granted, we can operate on any type of arraystrings, numbers, objects, even other arraysbut because sort is an instance method, we have to call it directly on the Array we want to sort.

We can also use libraries of true FP functions to do the same thing, but without binding our function to a specific array instance. Compare, for example, the Ramda map function to the built-in, vanilla JS Array.map method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { map } from 'ramda'

const squares = [1, 4, 9, 16, 25]

// Vanilla JS with the Array.map method
console.log('Array.map', squares.map(Math.sqrt))

// The Ramda map equivalent
console.log('Ramda map', map(Math.sqrt, squares))

// But the Ramda map is curried!
const mapSquareRoots = map(Math.sqrt)

console.log('[1, 4, 9, 16, 25]', mapSquareRoots([1, 4, 9, 16, 25]))
console.log('[36, 49, 64, 81, 100]', mapSquareRoots([36, 49, 64, 81, 100]))

As you can see, Ramda's pure map function is more powerful and reusable than even the Array.map method. We'll explain currying and composition in a moment.

Declarative vs. imperative style

Functional programming is declarative, meaning that the program logic is expressed without explicitly describing the flow control: it tells the code what to do rather than telling it how to do it.

Imperative programs require many lines of code to describe the specific steps needed to achieve the desired resultsthe flow control: How to do things.

Declarative programs abstract the flow control process, and instead use many fewer lines of code to describe the data flow: What to do.

This means that under the covers, declarative languages are imperative. At some level, the computer only responds to "how". But by abstracting the "how" into higher-level "what", functional programming makes life much easier for the programmer.

Loops are a good example of imperative code. At a quick glance, what does the code below do?

1
2
3
4
5
6
7
8
9
10
11
// This is not a good way to do things
const nums = [1, 2, 3, 4, 5]
const square = x => x * x

let out = []

for (let i = 0; i < nums.length; i++) {
  out[i] = square(nums[i])
}

console.log('by for loop', out)

Notice how you have to work your way through the for loop syntax. There's a lot going on. Now compare that to the declarative version:

1
2
3
4
5
6
7
8
9
10
11
12
// This is a much better way to code
import { map } from 'ramda'

const nums = [1, 2, 3, 4, 5]
const square = x => x * x

let out = map(square, nums) // It's almost plain English

console.log('by function', out)

// Or, using the Array.map method (still better than a loop)
console.log('by method', nums.map(square))

Imperative code frequently utilises statements. A statement is a piece of code which performs some action. Examples of commonly used statements include for, if, switch, throw, etc.

Declarative code relies more on expressions. An expression is a piece of code which evaluates to some value. Expressions are usually some combination of function calls, values, and operators which are evaluated to produce the resulting value. Examples include:

  • 2 * 2
  • doubleMap([2, 3, 4])
  • Math.max(4, 3, 2)
  • etc.

Functional composition

Function composition is the act of combining multiple functions together in order to create more complex logical flows.

The precondition that we need to bear in mind is this: when we compose functions, each one should take as an argument the return value of the previous function in the pipe.

If we had to add two numbers and then multiply the result of the addition by 10, then we might write something like this:

1
2
3
4
const add = (a, b) => a + b
const multiplyByTen = x => x * 10

const result = multiplyByTen(add(3, 5)) // 10 * (3 + 5)

The code is easy to understand but if we had to perform more operations readability suffers:

1
2
3
4
5
const sum = (a, b) => a + b
const square = x => x * x
const addTen = x => x + 10

const computeNumbers = addTen(square(sum(3, 5))) // 74

The deeper the nesting, the more difficult to read, though formatting can help:

1
2
3
4
5
6
7
8
9
const sum = (a, b) => a + b
const square = x => x * x
const addTen = x => x + 10

const computeNumbers = addTen(
  square(
    sum(3, 5)
  )
) // 74

We can use a third-party library such as Ramda to enable us to write functional code easily and cleanly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { add, compose, negate, pipe, sum } from 'ramda'

// compose applies the functions from  right to left
const compute = compose(
  negate,   // 3. Negate (= -25)
  add(10),  // 2. Add 10 (= 25)
  sum       // 1. Sum 1-5 (= 15)
)

console.log('Right to left', compute([1, 2, 3, 4, 5])) // -25

// pipe applies the functions from left to right
const computeAgain = pipe(
  sum,      // 1. Sum 1-5 (= 15)
  add(10),  // 2. Add 10 (= 25)
  negate    // 3. Negate (= -25)
)

console.log('Left to right', computeAgain([1, 2, 3, 4, 5]))

Currying

A curried function is a function that permits you to pass one parameter at a time. If you pass fewer arguments than the function expects, then it returns a function that takes the remaining arguments and remembers the arguments already passed.

Essentially, you can "partially apply" curried functions, creating new functions that remember the arguments already passed (this is called a "closure"). You can see this in the composition example above with the add function that takes two parameters, but is partially applied with the first parameter, add(10) to create a new function that will take a single number and add 10 to it.

An easy way to "curry" JavaScript functions is by nesting single-parameter functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
const add = x => y => x + y

// Now we can partially apply add:
const addTen = add(10)

// And then use it
console.log('addTen(15) is:', addTen(15))

// We can still pass both arguments at once, but as two calls:
console.log('add(10)(5) is:', add(10)(5))

// But we can't use the function normally:
console.log('add(10, 5) is:', add(10, 5))

In the above example, add(10, 5) returned a function because it ignored the second argument, so instead it worked the same as add(10): we got back a function that remembers the 10 and takes the second parameter. That's because this simplified method for currying means that we can only supply arguments one at a time.

What we want is a function that let's us apply the arguments in any combination. Ramda provides us with just such a function:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { curry } from 'ramda'

const addEmUp = curry((x, y, z) => x + y + z)

console.log('addEmUp(1, 2, 3) is:', addEmUp(1, 2, 3))
console.log('addEmUp(1)(2, 3) is:', addEmUp(1)(2, 3))
console.log('addEmUp(1, 2)(3) is:', addEmUp(1, 2)(3))
console.log('addEmUp(1)(2)(3) is:', addEmUp(1)(2)(3))

const addOne = addEmUp(1)
const addOneAndTwo = addOne(2)

console.log('addOneAndTwo(3) is:', addOneAndTwo(3))

Currying shines during refactoring when you create a generalized version of a function with lots of parameters and then use it to create specialized versions with fewer parameters. This way we can write more descriptive code and achieve much better code reusability.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { curry } from 'ramda'

const fetchData = curry((method, url, data) => {
  console.log(`Sending a ${method} request to ${url} with query ${data}.`)
})

const postRequest = fetchData('POST')
const postUser = postRequest('/users')
const postPet = postRequest('/pets')

postUser(JSON.stringify({ nameGiven: 'Bob', nameFamily: 'Dobbs' }))
postUser(JSON.stringify({ nameGiven: 'Bob', nameFamily: 'Dole' }))

postPet(JSON.stringify({ name: 'Bob', species: 'dog' }))

We can easily write our own function to curry other functions (as long as they take a fixed number of argumentscan you guess why?). So if you're into impressing your friends (or just curious), check out the example below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// curryN takes the number of parameters and the function
const curryN = (n, f) => (...args) => // return a function that takes some arguments
  args.length === n  // if all the arguments have been supplied
    ? f(...args)     // then call the function with the args and return the result
    : curryN(
      n - args.length,
      (...newArgs) => f(...args, ...newArgs)
    )                // else recurse for the remaining arguments

const curry = f => curryN(f.length, f) // Create a closure on the number of parameters
  
// A function that takes 3 parameters
const uncurried = (x, y, z) => ([x, y, z])

// Try applying it without currying
console.log(`uncurried(1, 2, 3) is ${JSON.stringify(uncurried(1, 2, 3))}`)

// Oops
try {
  console.log(`uncurried(1, 2)(3) is ${JSON.stringify(uncurried(1, 2)(3))}`)
  console.log(`uncurried(1)(2, 3) is ${JSON.stringify(uncurried(1)(2, 3))}`)
  console.log(`uncurried(1)(2)(3) is ${JSON.stringify(uncurried(1)(2)(3))}`)
} catch (e) {
  console.log(`Uh, oh: ${e}`)
}

// Now let's try again, but with our function curried properly
const curried = curry((x, y, z) => ([x, y, z]))

// Everything works
console.log(`curried(4, 5, 6) is ${JSON.stringify(curried(4, 5, 6))}`)
console.log(`curried(4, 5)(6) is ${JSON.stringify(curried(4, 5)(6))}`)
console.log(`curried(4)(5, 6) is ${JSON.stringify(curried(4)(5, 6))}`)
console.log(`curried(4)(5)(6) is ${JSON.stringify(curried(4)(5)(6))}`)

// We can partially apply our function to create new functions
// that "remember" one or more arguments
const prependSeven = curried(7)
const prependSevenAndEight = prependSeven(8)
console.log(`prependSevenAndEight(9) is ${JSON.stringify(prependSevenAndEight(9))}`)

Curry FTW.

More things to avoid

An excellent first step towards writing good functional code is to stop using the vanilla JS constructs below as they break one or more FP principles.

  • Loops (loops lead to imperative code)
    • while
    • do...while
    • for
    • for...of
    • for...in
  • Void functions (a void function returns nothing so is impure)
  • Variable declarations with var or let (permit mutation of state)
  • Object mutation, for example: obj.x = 5 (permit mutation of state)
  • Array mutator methods (these mutate the Array in place)
    • copyWithin
    • fill
    • pop
    • push
    • reverse
    • shift
    • sort
    • splice
    • unshift
  • Map mutator methods (these mutate the Map in place)
    • clear
    • delete
    • set
  • Set mutator methods (these mutate the Set in place)
    • add
    • clear
    • delete
Errors, bugs, suggestions, questions? Contact Prateek Sharma.