Skip to content

thinkswan/js-functional-programming-examples

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 

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

Watchers

Forks

Releases

No releases published

Packages

No packages published