Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 

Repository files navigation

Functional programming basics in ES6

These notes are taken from Jeremy Fairbank's talk, "Functional Programming Basics in ES6".

What is functional programming?

It's a paradigm that uses pure functions to build up higher-order functions.

A function simply maps an input (domain) to an output (range).

Why use functional programming?

  • Predictable: Pure, declarative functions
  • Safe: State is immutable
  • Transparent: State is first-class
  • Modular: Compose first-class functions

Functional programming libs

  • React
  • Redux
  • Lodash
  • Ramda

ES6 features useful in functional programming

Arrow functions

const add = (x, y) => x + y // add(2, 3) === 5
const identity = x => x // identity(1) === 1

Rest-spread operator

const array = (...elements) => elements // array(1, 2, 3) == [1, 2, 3]
const log = (...args) => console.log(...args) // log('Hello', 'Poznań') == 'Hello Poznań'

Destructuring

const [js, ...rest] = ["JavaScript", "Ruby", "Haskell"] // js === 'JavaScript', rest == ['Ruby', 'Haskell']
const head = ([x]) => x // head([1, 2, 3]) === 1

Default arguments

const greet = (name, greeting = "Hello") => console.log(greeting, name) // greet('Poznań') == 'Hello Poznań'

Object merging

Object.assign({}, { hello: "Poznań" }, { hi: "Warsaw" }) // { hello: 'Poznań', hi: 'Warsaw' }

ES6 classes

class Point {
  // Constructors desugar to functions, eg. function Point(x, y) {}
  constructor(x, y) {
    this.x = x
    this.y = y
  }

  // Instance methods desugar to prototype methods, eg. Point.prototype.moveBy = function(dx, dy) {}
  moveBy(dx, dy) {
    this.x == dx
    this.y == dy
  }
}

Predictable: Pure, declarative functions

Pure functions respect the following criteria:

  • No side effects (including mutations and printing)
  • No dependencies (including global state)
  • Idempotent (inputs always map to the same outputs, regardless of how many times the function is called)

To illustrate the benefits of pure functions, consider these impure functions:

let name = "Alice"

// Depends on global variable
const getName = () => name

// Mutates state
const setName = newName => (name = newName)

// Depends on global variable _and_ mutates state
const printUpperName = () => console.log(name.toUpperCase())

These functions are difficult to test:

describe("api", () => {
  beforeEach(() => mockConsoleLog())
  afterEach(() => restoreConsoleLog())

  it("sets and prints the name", () => {
    printUpperName()

    expect(console.log).calledWith("ALICE")

    setName("Bob")
    printUpperName()

    expect(console.log).calledWith("BOB")
  })
})

How can we rewrite this example as a pure function with tests?

const upperName = name => name.toUpperCase()

describe("api", () => {
  it("returns an uppercase name", () => {
    expect(upperName("Alice").to.equal("ALICE"))
    expect(upperName("Bob").to.equal("BOB"))
  })
})

An imperative function describes how to achieve the result. Consider this function:

const doubleNumbers = numbers => {
  const doubled = []

  for (let i = 0; i < numbers.length; i++) {
    doubled.push(numbers[i] * 2)
  }

  return doubled
}

doubleNumbers([1, 2, 3]) // [2, 4, 6]

A declarative function declares what the desired result is. To rewrite the above function declaratively:

const doubleNumbers = numbers => numbers.map(n => n * 2)

doubleNumbers([1, 2, 3]) // [2, 4, 6]

Safe: State is immutable

State should be created, not mutated.

To illustrate the benefits of immutable state, consider this example:

const hobbies = ["programming", "reading", "music"]

const firstTwo = hobbies.splice(0, 2) // ['programming', 'reading']

console.log(hobbies) // ['music']

One way to enforce immutable state is to use Object#freeze:

const hobbies = Object.freeze[("programming", "reading", "music")]

const firstTwo = hobbies.splice(0, 2) // TypeError

Another approach is to free the state. Consider the Point class from earlier:

class Point {
  constructor(x, y) {
    this.x = x
    this.y = y
  }

  moveBy(dx, dy) {
    this.x == dx
    this.y == dy
  }
}

const point = new Point(0, 0)

point.moveBy(5, 5)
point.moveBy(-2, 2)

console.log([point.x, point.y]) // [3, 7]

We can free the state in this class as follows:

const createPoint = (x, y) => Object.freeze([x, y])

const movePointBy = ([x, y], dx, dy) => Object.freeze([x + dx, y + dy])

let point = createPoint(0, 0)

point = movePointBy(point, 5, 5)
point = movePointBy(point, -2, 2)

console.log(point) // [3, 7]

Since immutable state requires us to return new data structures with each call, it has some pros and cons:

  • Pros
    • Safety
    • Free undo/redo logs (eg. Redux)
    • Explicit flow of data
    • Concurrency safety
  • Cons
    • Verbose
    • More object creation
    • More garbage collections
    • More memory usage

Modular: Compose first-class functions

JavaScript treats functions as first-class citizens. This means you can assign them to variables, pass them as input, and receive them as ouput, just like you can a boolean, number, or string.

Before continuing, we should define a few terms:

  • Higher-order functions return a new function.
  • Closures encapsulate state.
  • Partially-applied functions return a new function with 1 or more of the inputs set (similar to bind).
  • Curryable functions are functions that can be partially-applied and will invoke once all inputs are set.

Consider the following example:

// This function is both a higher-order function (because it returns a new function) and a closure (because it "closes over" `x`)
const createAdder = x => y => x + y

// This function is a partially-applied function (because it applies `x = 3`, but not `y`)
const add3 = createAdder(3)

add3(2) // 5
add3(3) // 6

Perhaps a more practical example:

const request = options => {
  return fetch(options.url, options).then(resp => resp.json())
}

const usersPromise = request({
  url: "/users",
  headers: { "X-Custom": "myKey" }
})
const tasksPromise = request({
  url: "/tasks",
  headers: { "X-Custom": "myKey" }
})

We can make this more reusable as follows:

const createRequester = options => {
  return otherOptions => request(Object.assign({}, options, otherOptions))
}

const customRequest = createRequester({ headers: { "X-Custom": "myKey" } })

const usersPromise = customRequest({ url: "/users" })
const tasksPromise = customRequest({ url: "/tasks" })

Moving on to curryable functions, let's recreate the adder and requester from above:

const add = x => y => x + y
const request = defaults => options => {
  options = Object.assign({}, defaults, options)

  return fetch(options.url, options).then(resp => resp.json())
}

With the building blocks of higher-order functions, closures, partially-applied functions, and curryable functions, let's look at a shopping cart example:

const map = fn => array => array.map(fn)
const multiply = x => y => x * y
const pluck = key => object => object[key]

const discount = multiply(0.98)
const tax = multiply(1.0925)
const customRequest = request({ headers: { "X-Custom": "myKey" } })

customRequest({ url: "/cart/items" }) // [{ price: 5 }, { price: 10 }, { price: 3 }]
  .then(map(pluck("price"))) // [5, 10, 3]
  .then(map(discount)) // [4.9, 9.8, 2.94]
  .then(map(tax)) // [5.35, 10.71, 3.21]

We can also compose closures:

const processWord = compose(
  hyphenate,
  reverse,
  toUpperCase
) // Same as `word => hyphenate(reverse(toUpperCase(word)))`

const words = ["hello", "functional", "programming"]

const newWords = words.map(processWord) // ['OL-LEH', 'LANOI-TCNUF', 'GNIMM-ARGORP']

To improve the performance of the shopping cart example, we can replace the 3 map interations with a single iteration:

customRequest({ url: "/cart/items" }) // [{ price: 5 }, { price: 10 }, { price: 3 }]
  .then(
    map(
      compose(
        tax,
        discount,
        pluck("price")
      )
    )
  ) // [5, 10, 3] => [4.9, 9.8, 2.94] => [5.35, 10.71, 3.21]

Finally, to handle loops in functional programming, we can use recursion. Consider a function that solves a factorial:

const factorial = n => {
  let result = 1

  while (n > 1) {
    result *= n
    n--
  }

  return result
}

To rewrite this recursively:

const factorial = n => {
  if (n < 2) return 1

  return n * factorial(n - 1)
}

To avoid exceeding the call stack size, we can optimize this further using tail call optimization:

const factorial = (n, accum = 1) => {
  if (n < 2) return accum

  return factorial(n - 1, n * accum)
}

About

Notes from Jeremy Fairbank's "Functional Programming Basics in ES6" talk.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors