Programming with Callbacks

Questions

  • What happens when a function is defined?
  • How can I pass a function to another function?
  • Why would I want to do that?
  • What are higher-order functions?
  • What higher-order functions do JavaScript arrays provide?
  • What is a closure?
  • When and why are closures useful?

Introduction

Functions as Parameters

FIXME-14: diagram

const doTwice = (action) => {
  action()
  action()
}

const hello = () => {
  console.log('hello')
}

doTwice(hello)
hello
hello

FIXME-14: diagram

const pipeline = (initial, first, second) => {
  return second(first(initial))
}

const trim = (text) => { return text.trim() }
const dot = (text) => { return text.replace(/ /g, '.') }

const original = '  this example uses text  '

const trimThenDot = pipeline(original, trim, dot)
console.log(`trim then dot: |${trimThenDot}|`)

const dotThenTrim = pipeline(original, dot, trim)
console.log(`dot then trim: |${dotThenTrim}|`)
trim then dot: |this.example.uses.text|
dot then trim: |..this.example.uses.text..|
const pipeline = (initial, operations) => {
  let current = initial
  for (let op of operations) {
    current = op(current)
  }
  return current
}

const trim = (text) => { return text.trim() }
const dot = (text) => { return text.replace(/ /g, '.') }
const double = (text) => { return text + text }

const original = ' some text '
const final = pipeline(original, [double, trim, dot])
console.log(`|${original}| -> |${final}|`)
| some text | -> |some.text..some.text|
const transform = (values, operation) => {
  let result = []
  for (let v of values) {
    result.push(operation(v))
  }
  return result
}

const data = ['one', 'two', 'three']

const upper = transform(data, (x) => { return x.toUpperCase() })
console.log(`upper: ${upper}`)

const first = transform(data, (x) => { return x[0] })
console.log(`first: ${first}`)
upper: ONE,TWO,THREE
first: o,t,t

Functional Programming

const data = ['this', 'is', 'a', 'test']
console.log('some longer than 3:', data.some((x) => { return x.length > 3 }))
console.log('all greater than 3:', data.every((x) => { return x.length > 3 }))
some longer than 3: true
all greater than 3: false
const data = ['this', 'is', 'a', 'test']
console.log('those greater than 3:', data.filter((x) => { return x.length > 3 }))
those greater than 3: [ 'this', 'test' ]
const data = ['this', 'is', 'a', 'test']
console.log('all longer than 3 start with t',
            data
            .filter((x) => { return x.length > 3 })
            .every((x) => { return x[0] === 't' }))
all longer than 3 start with t true
const data = ['this', 'is', 'a', 'test']
console.log('shortened', data.map((x) => { return x.slice(0, 2) }))
shortened [ 'th', 'is', 'a', 'te' ]
const data = ['this', 'is', 'a', 'test']

const concatFirst = (accumulator, nextValue) => {
  return accumulator + nextValue[0]
}
const acronym = data.reduce(concatFirst, '')
console.log(`acronym of ${data} is ${acronym}`)

console.log('in one step', data.reduce((accum, next) => {
  return accum + next[0]
}, ''))
acronym of this,is,a,test is tiat
in one step tiat

Closures

const pipeline = (initial, operations) => {
  let current = initial
  for (let op of operations) {
    current = op(current)
  }
  return current
}
const adder = (increment) => {
  const f = (value) => {
    return value + increment
  }
  return f
}

const add1 = adder(1)
const add2 = adder(2)
console.log(`add1(100) is ${add1(100)} and add2(100) is ${add2(100)}`)
add1(100) is 101 and add2(100) is 102

FIXME-14: diagram

FIXME-14: diagram

FIXME-14: diagram

FIXME-14: diagram

FIXME-14: diagram

const pipeline = as before

const adder = as before

const result = pipeline(100, [adder(1), adder(2)])
console.log(`adding 1 and 2 to 100 -> ${result}`)
adding 1 and 2 to 100 -> 103
const pipeline = (initial, operations) => {
  let current = initial
  for (let op of operations) {
    current = op(current)
  }
  return current
}

const result = pipeline(100, [(x) => x + 1, (x) => x + 2])
console.log(`adding 1 and 2 to 100 -> ${result}`)
adding 1 and 2 to 100 -> 103

Challenges

Side Effects With forEach

JavaScript arrays have a method called forEach, which calls a callback function once for each element of the array. Unlike map, forEach does not save the values returned by these calls or return an array of results. The full syntax is:

someArray.forEach((value, location, container) => {
  // 'value' is the value in 'someArray'
  // 'location' is the index of that value
  // 'container' is the containing array (in this case, 'someArray')
}

If you only need the value, you can provide a callback that only takes one parameter; if you only need the value and its location, you can provide a callback that takes two. Use this to write a function doubleInPlace that doubles all the values in an array in place:

const vals = [1, 2, 3]
doubleInPlace(vals)
console.log(`vals after change: ${vals}`)
vals after change: 2,4,6

Annotating Data

Given an array of objects representing observations of wild animals:

data = [
  {'date': '1977-7-16', 'sex': 'M', 'species': 'NL'},
  {'date': '1977-7-16', 'sex': 'M', 'species': 'NL'},
  {'date': '1977-7-16', 'sex': 'F', 'species': 'DM'},
  {'date': '1977-7-16', 'sex': 'M', 'species': 'DM'},
  {'date': '1977-7-16', 'sex': 'M', 'species': 'DM'},
  {'date': '1977-7-16', 'sex': 'M', 'species': 'PF'},
  {'date': '1977-7-16', 'sex': 'F', 'species': 'PE'},
  {'date': '1977-7-16', 'sex': 'M', 'species': 'DM'}
]

write a function that returns a new array of objects like this:

newData = [
  {'seq': 3, 'year': '1977', 'sex': 'F', 'species': 'DM'},
  {'seq': 7, 'year': '1977', 'sex': 'F', 'species': 'PE'}
]

without using any loops. The changes are:

  • The date field is replaced with just the `year.
  • Only observations of female animals are retained.
  • The retained records are given sequence numbers to relate them back to the original data. (These sequence numbers are 1-based rather than 0-based.)

You may want to use Array.reduce to generate the sequence numbers. Use the web to find a description of it, and work with a partner to ensure that you understand how it works.

Key Points

  • JavaScript stores the instructions making up a function in memory like any other object.
  • Function objects can be assigned to variables, put in lists, passed as arguments to other functions, etc.
  • Functions can be defined in place without ever being given a name.
  • A callback function is one that is passed in to another function for it to execute at a particular moment.
  • Functional programming uses higher-order functions on immutable data.
  • Array.some is true if any element in an array passes a test, while Array.every is true if they all do.
  • Array.filter selects elements of an array that pass a test.
  • Array.map creates a new array by transforming values in an existing one.
  • Array.reduce reduces an array to a single value.
  • A closure is a set of variables captured during the definition of a function.