Commit 53fbefe6 authored by Caleb C. Sander's avatar Caleb C. Sander
Browse files

Initial commit

parents
Showing with 1964 additions and 0 deletions
+1964 -0
# CS 11 Asynchronous Programming track (SP 2020)
## Course description
TODO
## Me description
TODO
## JavaScript reference
There is a description of the important parts of the JavaScript language in [`notes/js/js.md`](notes/js/js.md).
It also has information specific to running JS in the browser and through Node.js.
You're welcome to read as much of it as you want, but it's quite long.
I will point out sections of it that may be useful on each assignment.
## Schedule
| Weeks | Topic and notes | Project | Due date |
| ----- |---------------- | ------- | -------- |
| 1-2 | [Callbacks](notes/callbacks/callbacks.md) | [Minesweeper](specs/minesweeper/minesweeper.md) | 2020-04-17 |
| 3 | [Promises](notes/promises/promises.md) | [`make`](specs/make/make.md) | 2020-04-24 |
| 4 | Streams | `grep` | 2020-05-01 |
| 5 | HTTP | Wiki Game | 2020-05-08 |
| 6 | WebSockets | Chat server | 2020-05-15 |
| 7 | | Chat client | 2020-05-22 |
| 8-9 | `async`-`await` | MiniVC | 2020-06-05 |
# Asynchronous idioms: event handlers and callbacks
## An example in the browser
JavaScript was designed as a programming language to run on webpages so that users can interact with the content on the page.
Some examples of things webpages often do in JavaScript:
- Show/hide part of the page when the user clicks on a button
- When the user enters some text in a form, check that it is a valid email address
- Update the state of a game when the user presses a key
- Upload data to a server
- Periodically request updated data from a server
[Here](click.html) is a simple example of JS code (between the `<script>` and `</script>` tags) that displays a message when the user clicks on a button:
```html
<html>
<body>
<button id='alert-button'>Click me!</button>
<script>
// We use the `id` property of the button to access it from JS
const button = document.getElementById('alert-button')
button.onclick = () => {
// You could put any other code you wanted here
// and it would run each time the button is clicked
alert('Button was clicked')
}
</script>
</body>
</html>
```
If you open this HTML file in the browser, it will show a button on the screen.
Each time the button is clicked, it displays a message saying that the button was clicked.
## How does this work?
Let's tease apart the code responsible for making button clicks display the message.
First, note that `() => alert('Button was clicked')` is a *function* in JS.
When this function is called, it causes the message `Button was clicked` to appear.
Then `button.onclick = ...` tells the browser "this is the function I want to attach to the click action on `button`.
When a user interacts with the webpage, the browser figures out whether the button was clicked, and if so, it runs the function stored in `button.onclick`.
## How doesn't this work?
Note that nowhere does our code say something like "wait until `button` is clicked" or "was `button` clicked yet?".
If you've dealt with user input in another language, you might have expected our JS code to handle button clicks to look more like:
```js
// 1.
while (true) {
button.waitForClick()
alert('Button was clicked')
}
// or
// 2.
while (true) {
if (button.wasClicked()) {
alert('Button was clicked')
}
}
```
So why doesn't JS use an interface like one of these?
1. We call a function `button.waitForClick()` that asks the browser to pause our code until the user clicks the button.
Although this could work in our simple example, what if the user might click either of two buttons and we want to respond to both?
If we call `button1.waitForClick()` and they click `button2` instead, we will fail to detect their click.
2. This approach solves the issue of not being able to respond to multiple events: we could just add an `if (button2.wasClicked())` inside the loop.
However, it has a more subtle problem.
The loop may run billions of times before the user actually clicks the button.
Since our program can't actually do anything until the user finally clicks the button, this is an incredible waste of the processor's time.
We see that the "event handler" approach used by JS has two main benefits: it can easily set up handlers for thousands of events at once, and no JS code runs while we wait for an event to occur.
## Combining asynchronous events in parallel
Imagine that we would like to wait until several events have all happened before running some code.
In [this example](lock1.html), we make a combination lock with 4 digits and want to take an action when all the digits are correct:
```js
// Adds a digit to the lock. When the digit changes,
// `changeCallback()` will be called with the new value.
function makeLockDigit(changeCallback) {
// ... (the full code is at the link above)
digit.onchange = () => changeCallback(Number(digit.value))
}
// The super secret combination
const COMBO = [1, 2, 3, 4]
// Whether each digit is correct
const digitCorrect = [false, false, false, false]
for (let i = 0; i < COMBO.length; i++) {
makeLockDigit(value => {
// Digit i has changed, so store whether it is correct
digitCorrect[i] = (value === COMBO[i])
// If all digits are now correct, show a message
if (digitCorrect.every(correct => correct)) {
alert('You got the combination')
}
})
}
```
Since any of the 4 digits might be the last one to become correct, we check whether all of the digits are correct whenever each one changes.
This is generally the case when we don't know what order some asynchronous events will finish in: whenever each one finishes, it needs to check if it was the last one to finish.
There are other ways to store the state of the digits; for example, we could instead track how many are currently correct.
[This version](lock2.html) of the code implements that approach:
```js
// The number of correct digits
let correctCount = 0
for (let i = 0; i < COMBO.length; i++) {
// Whether this digit was previously correct
let wasCorrect = false
makeLockDigit(value => {
// Compare the digit's actual value to the correct value
const isCorrect = (value === COMBO[i])
// Update the number of correct digits if this one changed
if (!wasCorrect && isCorrect) correctCount++
else if (wasCorrect && !isCorrect) correctCount--
wasCorrect = isCorrect
// If all digits are now correct, show a message
if (correctCount === COMBO.length) {
alert('You got the combination')
}
})
}
```
## `setTimeout()`
Another asynchronous JS API is the function `setTimeout()`, which is used to wait for time to pass.
For example, to display a message after 3 seconds (3000 ms), you would write
```js
setTimeout(() => {
console.log('3 seconds have passed')
}, 3000)
```
The fact that `setTimeout()` is asynchronous means it returns immediately, without witing for the time interval.
For example, the following code displays `Timeout started` immediately and `Timeout ended` after 1 second:
```js
setTimeout(() => {
console.log('Timeout ended')
}, 1000)
console.log('Timeout started')
```
As with event handlers, because this API is asynchronous, you can easily wait for multiple intervals of time to pass at the same time.
For example, to create events that will finish 1, 2, ..., 10 seconds from now:
```js
for (let i = 1; i <= 10; i++) {
setTimeout(() => {
console.log(`${i} seconds passed`)
}, i * 1000)
}
```
## Combining asynchronous events in sequence
It is also common to have one asynchronous event that depends on the result of another.
For example, we might want to wait for the user to enter some text, then send it to a server and wait for the server to respond.
In general, this requires us to create the next asynchronous event *inside* the handler for the previous one.
As a concrete example, let's simulate a "random walk" on a grid.
At each step, we randomly move left, right, up, or down, and we count the total number of times we have visited each grid square.
Here is a [first attempt](walk1.html) that only moves 3 times.
```js
// ... (the full code is at the link above)
// Visit a given grid position and
// display the number of times it has been visited
function visit(position) {
// Don't do anything if we're off the grid
const row = grid[position.row]
if (row === undefined) return
const cell = row[position.col]
if (cell === undefined) return
cell.count++
cell.element.innerText = String(cell.count)
}
// Compute the next position by randomly going left, right, up, or down
function nextPosition(position) {
const {row, col} = position
const random = Math.random()
if (random < 0.25) return {row: row + 1, col}
else if (random < 0.5) return {row: row - 1, col}
else if (random < 0.75) return {row, col: col + 1}
else return {row, col: col - 1}
}
// Start in the middle of the grid and jump every DELAY ms
let position = {row: Math.floor(SIZE / 2), col: Math.floor(SIZE / 2)}
visit(position)
setTimeout(() => {
// First movement
position = nextPosition(position)
visit(position)
setTimeout(() => {
// Second movement
position = nextPosition(position)
visit(position)
setTimeout(() => {
// Third movement
position = nextPosition(position)
visit(position)
}, DELAY)
}, DELAY)
}, DELAY)
```
You can see that each sequential delay is represented by nesting another `setTimeout()` inside the last one.
If we want to keep jumping to new squares forever, we need to [rewrite this program using a recursive function](walk2.html):
```js
function walkFrom(position) {
// To perform the walk from the given starting position,
// visit it and then start from the next position after a delay
visit(position)
setTimeout(() => walkFrom(nextPosition(position)), DELAY)
}
// Start in the middle of the grid
walkFrom({row: Math.floor(SIZE / 2), col: Math.floor(SIZE / 2)})
```
## Conclusion
We have seen two examples of asynchronous standard library functions in JavaScript: handling user input and waiting for time.
With both of them, we define a function that we want to get called when something happens.
This structure is common to any asynchronous function; the function passed in to run when it finishes is refered to as the "callback function".
It is up to the browser (or Node.js) to ensure that the callbacks we provide get called at the right times.
We have also looked at some of the primary ways to combine asynchronous actions.
Most programs that interact with a user, read/write files, make web requests, etc. can be broken into actions that run sequentially and in parallel.
Asynchronous programming makes it simple to run actions in parallel or in sequence, although running an action sequentially in a "loop" requires recursion.
<html>
<body>
<button id='alert-button'>Click me!</button>
<script>
// We use the `id` property of the button to access it from JS
const button = document.getElementById('alert-button')
button.onclick = () => {
// You could put any other code you wanted here
// and it would run each time the button is clicked
alert('Button was clicked')
}
</script>
</body>
</html>
<html>
<body>
<div id='lock'></div>
<script>
const lock = document.getElementById('lock')
function makeLockDigit(changeCallback) {
// Make an input that can be set between 0 and 9.
// This is equivalent to <input type='number' min='0' max='9' />.
const digit = document.createElement('input')
digit.type = 'number'
digit.min = 0
digit.max = 9
// Start it with a random value
digit.value = String(Math.floor(Math.random() * 10))
// When the digit changes, call `changeCallback()`,
// passing the new value of the digit.
// Inputs' values are strings, so we convert them to numbers.
digit.onchange = () => changeCallback(Number(digit.value))
// Add the digit to the lock
lock.appendChild(digit)
}
// The super secret combination
const COMBO = [1, 2, 3, 4]
// Whether each digit is correct
const digitCorrect = [false, false, false, false]
for (let i = 0; i < COMBO.length; i++) {
makeLockDigit(value => {
// Digit i has changed, so store whether it is correct
digitCorrect[i] = (value === COMBO[i])
// If all digits are now correct, show a message
if (digitCorrect.every(correct => correct)) {
alert('You got the combination')
}
})
}
</script>
</body>
</html>
<html>
<body>
<div id='lock'></div>
<script>
const lock = document.getElementById('lock')
function makeLockDigit(changeCallback) {
// Make an input that can be set between 0 and 9.
// This is equivalent to <input type='number' min='0' max='9' />.
const digit = document.createElement('input')
digit.type = 'number'
digit.min = 0
digit.max = 9
// Start it with a random value
digit.value = String(Math.floor(Math.random() * 10))
// When the digit changes, call `changeCallback()`,
// passing the new value of the digit.
// Inputs' values are strings, so we convert them to numbers.
digit.onchange = () => changeCallback(Number(digit.value))
// Add the digit to the lock
lock.appendChild(digit)
}
// The super secret combination
const COMBO = [1, 2, 3, 4]
// The number of correct digits
let correctCount = 0
for (let i = 0; i < COMBO.length; i++) {
// Whether this digit was previously correct
let wasCorrect = false
makeLockDigit(value => {
// Compare the digit's actual value to the correct value
const isCorrect = (value === COMBO[i])
// Update the number of correct digits if this one changed
if (!wasCorrect && isCorrect) correctCount++
else if (wasCorrect && !isCorrect) correctCount--
wasCorrect = isCorrect
// If all digits are now correct, show a message
if (correctCount === COMBO.length) {
alert('You got the combination')
}
})
}
</script>
</body>
</html>
<html>
<body>
<table id='grid'></table>
<script>
const SIZE = 30 // width and height of the grid
const DELAY = 500 // delay in ms between steps of the walk
// Build the SIZE x SIZE grid
const table = document.getElementById('grid')
// `grid[i][j]` stores the table cell in row `i` and column `j`,
// as well as the number of times it has been visited during the walk
const grid = []
for (let i = 0; i < SIZE; i++) {
// `tr` is a "table row" element
const rowElement = document.createElement('tr')
const row = []
for (let j = 0; j < SIZE; j++) {
// `td` is a "table cell" element
const element = document.createElement('td')
rowElement.appendChild(element)
row.push({element, count: 0})
}
table.appendChild(rowElement)
grid.push(row)
}
// Visit a given grid position and
// display the number of times it has been visited
function visit(position) {
// Don't do anything if we're off the grid
const row = grid[position.row]
if (row === undefined) return
const cell = row[position.col]
if (cell === undefined) return
cell.count++
cell.element.innerText = String(cell.count)
}
// Compute the next position by randomly going left, right, up, or down
function nextPosition(position) {
const {row, col} = position
const random = Math.random()
if (random < 0.25) return {row: row + 1, col}
else if (random < 0.5) return {row: row - 1, col}
else if (random < 0.75) return {row, col: col + 1}
else return {row, col: col - 1}
}
// Start in the middle of the grid and jump every DELAY ms
let position = {row: Math.floor(SIZE / 2), col: Math.floor(SIZE / 2)}
visit(position)
setTimeout(() => {
// First movement
position = nextPosition(position)
visit(position)
setTimeout(() => {
// Second movement
position = nextPosition(position)
visit(position)
setTimeout(() => {
// Third movement
position = nextPosition(position)
visit(position)
}, DELAY)
}, DELAY)
}, DELAY)
</script>
<style>
#grid {
border-collapse: collapse;
}
#grid td {
border: 1px solid black;
width: 20px;
height: 20px;
font-size: 15px;
font-family: sans-serif;
text-align: center;
}
</style>
</body>
</html>
<html>
<body>
<table id='grid'></table>
<script>
const SIZE = 30 // width and height of the grid
const DELAY = 500 // delay in ms between steps of the walk
// Build the SIZE x SIZE grid
const table = document.getElementById('grid')
// `grid[i][j]` stores the table cell in row `i` and column `j`,
// as well as the number of times it has been visited during the walk
const grid = []
for (let i = 0; i < SIZE; i++) {
// `tr` is a "table row" element
const rowElement = document.createElement('tr')
const row = []
for (let j = 0; j < SIZE; j++) {
// `td` is a "table cell" element
const element = document.createElement('td')
rowElement.appendChild(element)
row.push({element, count: 0})
}
table.appendChild(rowElement)
grid.push(row)
}
// Visit a given grid position and
// display the number of times it has been visited
function visit(position) {
// Don't do anything if we're off the grid
const row = grid[position.row]
if (row === undefined) return
const cell = row[position.col]
if (cell === undefined) return
cell.count++
cell.element.innerText = String(cell.count)
}
// Compute the next position by randomly going left, right, up, or down
function nextPosition(position) {
const {row, col} = position
const random = Math.random()
if (random < 0.25) return {row: row + 1, col}
else if (random < 0.5) return {row: row - 1, col}
else if (random < 0.75) return {row, col: col + 1}
else return {row, col: col - 1}
}
function walkFrom(position) {
// To perform the walk from the given starting position,
// visit it and then start from the next position after a delay
visit(position)
setTimeout(() => walkFrom(nextPosition(position)), DELAY)
}
// Start in the middle of the grid
walkFrom({row: Math.floor(SIZE / 2), col: Math.floor(SIZE / 2)})
</script>
<style>
#grid {
border-collapse: collapse;
}
#grid td {
border: 1px solid black;
width: 20px;
height: 20px;
font-size: 15px;
font-family: sans-serif;
text-align: center;
}
</style>
</body>
</html>
<html>
<body>
<!--
A simple calculator that only supports addition.
It has an input where you can enter a number to add,
a button that can be clicked to add it,
and text that displays the current sum.
-->
<input type='number' id='number' />
<button id='add'>+</button>
<div id='sum'></div>
<!--The JavaScript code that makes the calculator work-->
<script>
// Get the elements declared above in order to interact with them
const numberInput = document.getElementById('number')
const sumDisplay = document.getElementById('sum')
// Show the current value of the `sum` variable in `<div id='sum'>`
function showSum() {
sumDisplay.innerText = String(sum)
}
// The sum starts at 0
let sum = 0
showSum()
// Every time `<button id='add'>` is clicked,
// add the number in `<input id='number'>` to the sum
document.getElementById('add').onclick = () => {
sum += Number(numberInput.value)
showSum()
}
</script>
</body>
</html>
notes/js/colors.png

27.9 KB

// Import the file system module
const fs = require('fs')
if (process.argv.length !== 4) {
throw new Error('Invalid syntax. Usage: node copy.js source destination')
}
// Extract the command line arguments
const source = process.argv[2]
const destination = process.argv[3]
// Read the contents of `source`
fs.readFile(source, (err, data) => {
if (err) throw err
// Write `data` to `destination`, which copies `source` to `destination`
fs.writeFile(destination, data, err => {
if (err) throw err
})
})
notes/js/dev-tools.png

98.8 KB

# JavaScript
## Contents
- [JavaScript language features](#the-javascript-language)
- [Basic types](#types): `Number`, `String`, `Boolean`, `Array`, `Object`, and `undefined`
- [Printing values](#console.log): `console.log`
- [Variables](#variables): `let`, `const`, and destructuring
- [Control flow](#control-flow)
- [`if`](#if-statements)
- [`while`](#while-loops); `break` and `continue`
- [`for (initializer; condition; increment)`](#c-style-for-loops)
- [`for (const value of iterable)`](#for-of-loops)
- [Functions](#functions)
- [Anonymous functions](#anonymous-functions): `=>`
- [Named functions](#function): `function`
- [Function arguments/parameters](#parameters)
- [Classes](#classes): `class`
- [Constructors](#constructor): `constructor`
- [Methods](#methods): instance and `static` methods
- [Inheritance](#inheritance): `extends`
- [`Set`](#set)
- [`Map`](#map)
- [Error handling](#error-handling): `Error`, `throw`, and `try`-`catch`
- [More function parameters](#advanced-function-parameters): `= default` arguments, `...` arguments, and destructuring arguments
- [JavaScript in the browser](#browser-side-javascript)
- [Example](#browser-side-example)
- [Documentation](#browser-side-documentation)
- [Node.js](#nodejs): server-side JavaScript
- [Running programs in Node.js](#running-the-interpreter)
- [Modules](#modules): Node.js builtin modules, `require`, and `module.exports`
- [Example](#nodejs-example)
- [npm](#installing-npm-packages): using open-source libraries with npm
- [Documentation](#nodejs-documentation)
---
## The JavaScript language
### Types
JavaScript is a dynamically-typed language, much like Python.
This means that writing JavaScript can require a lot of time spent debugging programs to figure out why values don't have the types you were expecting.
JavaScript also has an annoying tendency to try to guess what you meant to write rather than immediately reporting an error.
(For example, what do you think `[] * 3` evaluates to?)
With that being said, these are the primary types in JavaScript:
- `Number`: this is a numeric type like `float` in Python or `double` in Java.
It mostly behaves as you'd expect, e.g. `1 + 2` is `3` and `1 / 3` is `0.3333333333333333`.
It supports the same operations as Python does: `+`, `-`, `*`, `/`, `%` (remainder), and `**` (exponentiation).
- `String`: this is a textual type like `str` in Python or `String` in Java.
- String literals can use single or double quotes, or backticks to embed other values in the string:
```js
'abc' // 'abc'
"abc" // 'abc'
const count = 2
`I have ${count} apples` // 'I have 2 apples'
```
- Strings can be concatenated with the `+` operator.
You can also extract characters using the `[]` operator, e.g. `'abc'[1]` is `'b'`.
- Strings have lots of useful methods: `toLowerCase()`, `slice()`, `repeat()`, etc. As in Python or Java, strings are immutable, so methods like `replace()` return a *new* string with the replacements.
- `Boolean`: `true` and `false`. Useful operators: `!` for negation, `&&` for "and", and `||` for "or".
Numbers and strings can also be treated as booleans (`0` and `''` are "falsy" and all other values are "truthy").
For example, `if (values.length)` checks whether the array `values` is not empty.
- `undefined`: this is a special value that is used to indicate "no value".
It is the default value of uninitialized variables, return values from functions, etc.
It is falsy.
- Arrays: this is a growable list type like `list` in Python or `List` in Java.
- Array literals look like `[1, 2, 3]`, just as in Python.
- Use the `[]` operator to get/set elements of an array:
```js
const arr = [1, 2, 3]
arr[0] = arr[2]
arr // [3, 2, 3]
```
- Arrays can be grown by setting an index past the end, or by calling `push()`:
```js
const arr = []
arr.length // 0
arr[0] = 1
arr // [1]
arr.push(2)
arr // [1, 2]
arr.length // 2
```
- Arrays have lots of useful [methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#Instance_methods), e.g. `map()`, `includes()`, and `some()`
- Objects: in JavaScript, all "objects" (in the Python/Java sense) are really just maps of strings to values.
However, they are not often used as maps since [`Map`](#map)s are much more convenient.
- Object literals look like `{key1: value1, key2: value2}`.
- Values can be get/set using the `[]` operator, or `.` notation:
```js
const point = {x: 1.5, y: 2.3}
point['x'] += 1
point // {x: 2.5, y: 2.3}
point.z = point.x * point.y
point // {x: 2.5, y: 2.3, z: 5.75}
point.noSuchKey // undefined
```
- There is a useful shorthand for putting variables into an object:
```js
const head = 'A'
const tail = {head: 'B'}
const listAB = {head, tail}
listAB // {head: 'A', tail: {head: 'B'}}
```
The functions `Number`, `String`, and `Boolean` can be used to convert between the primitive types.
For example, `Number('123')` is `123`.
### `console.log()`
`console.log()` is JavaScript's equivalent of `print()` in Python or `System.out.println()` in Java.
It can be very helpful when debugging.
It is a builtin function that you can use in browser-side JS or Node.js.
In Node.js, `console.log()` prints to the terminal.
In browsers, `console.log()` prints values to the developer console.
For example, in Chrome:
![Chrome developer tools](dev-tools.png)
### Variables
JavaScript has two types of variables: `let` and `const`.
Variables declared as `let` can be given a new value, but `const` variables can only have their value set once:
```js
let collatz = 27
while (collatz > 1) {
console.log(collatz)
if (collatz % 2 === 0) collatz /= 2
else collatz = 3 * collatz + 1
}
// vs.
const collatz = 27
collatz = 3 * collatz + 1
// Uncaught TypeError: Assignment to constant variable.
```
(Note that `const` doesn't mean "immutable"; the array and object examples above show that we can *modify* an array or object referenced by a `const` variable.)
I find that around 90% of my variables can be `const`, so I recommend declaring variables `const` whenever possible to avoid unexpectedly changing them.
Both types of variables are only visible in the scope where they are defined:
```js
const x = 'outer'
if (true) {
const y = 'inner'
x // 'outer'
y // 'inner'
}
x // 'outer'
y // Uncaught ReferenceError: y is not defined
```
`let` variables have the value `undefined` by default.
```js
let max
max // undefined
const values = [4, 3, 1, 5, 2]
for (const value of values) {
if (max === undefined || value > max) max = value
}
max // 5
```
There are two other ways you can assign to a variable: "array destructuring" and "object destructuring".
You can think of these as the opposites of array literals and object literals.
```js
const arr = 'abcdef'.split('')
arr // ['a', 'b', 'c', 'd', 'e', 'f']
const [a, b, ...rest] = arr
a // 'a'
b // 'b'
rest // ['c', 'd', 'e', 'f']
const obj = {mean: 3.1, standardDeviation: 0.57, median: 4}
const {mean, standardDeviation} = obj
mean // 3.1
standardDeviation // 0.57
```
### Control flow
JavaScript has the standard comparison operators `<`, `<=`, `>`, and `>=`.
The major distinction from Python and Java is that we use the `===` ("triple-equals") and `!==` operators to check for equality and inequality!
The `==` and `!=` operators in JS have unpredictable behavior when comparing different types of values, so don't use them!
It's also important to note that `===` on objects (or arrays) returns whether they are *exactly the same object*, as in Java, but unlike in Python:
```js
const a = []
const b = a // a and b refer to the same array; changing one changes the other
const c = [] // c is a different array that also happens to be empty
a === b // true
a === c // false
```
#### `if` statements
`if` statements work exactly like in Python or Java: they run some code only when a condition is `true`, and possibly some other code if the condition is `false`.
The syntax matches Java (or any other language inspired by C):
```js
let a
let b
if (Math.random() < 0.5) {
a = ['a', 'p', 'e']
b = ['a', 'n', 'd']
}
else {
a = ['d', 'o', 'g']
b = ['d', 'o', 'n']
}
// Find which of a or b comes earlier in dictionary order
let min
if (a[0] < b[0]) min = a
else if (a[0] > b[0]) min = b
else if (a[1] < b[1]) min = a
else if (a[1] > b[1]) min = b
else if (a[2] < b[2]) min = a
else if (a[2] > b[2]) min = b
else min = a // they are equal
min // ['a', 'n', 'd'] or ['d', 'o', 'g']
```
#### `while` loops
`while` statements are also identical to Python and Java: they run some code repeatedly while a certain condition is met.
For example, computing `10 ** 13` by repeated squaring:
```js
let base = 10
let exponent = 13
let power = 1
while (exponent > 0) {
const addPower = exponent % 2
if (addPower === 1) power *= base
base *= base
exponent = (exponent - addPower) / 2
}
power // 10000000000000
```
As in Python or Java, `break` can be used to quit a loop early and `continue` can be used to skip to the next iteration of the loop.
These also work with both types of `for` loops.
For example, to find a multiple of 3 in an array:
```js
const values = []
while (values.length < 10) {
values.push(Math.floor(Math.random() * 10))
}
let foundIndex
let testIndex = 0
while (testIndex < values.length) {
if (values[testIndex] % 3 === 0) {
foundIndex = testIndex
break // stop as soon as a multiple of 3 is found
}
testIndex++
}
```
#### C-style `for` loops
`for` loops consist of 3 parts: an initialization statement, a `while` condition, and an update statement.
As in C or Java, these loops are often used to loop over all integers in a range.
For example, to randomly shuffle an array:
```js
const arr = []
for (let i = 0; i < 10; i++) arr.push(i)
arr // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// For each index i, choose one of the i + 1 unused elements to go there
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
const temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
arr // [6, 4, 3, 2, 9, 8, 5, 1, 0, 7] (results will vary)
```
#### `for`-`of` loops
The `for`-`of` loop is similar to Python's `for`-`in` loop or Java's `for`-`:` loop: it iterates over each item of a collection.
Be careful not to confuse this with JS's `for`-`in` loop, which behaves very differently!
Typically, `for`-`of` loops are used to iterate over arrays, but they work with any "iterable" values, such as [`Map`](#map) and [`Set`](#set).
For example, to compute an average:
```js
// Sample a million values uniformly between 0 and 1 and square them
const values = []
while (values.length < 1e6) {
values.push(Math.random() ** 2)
}
let sum = 0
for (const value of values) sum += value
const average = sum / values.length
average // 0.3332654911154851 (results will vary)
```
Or, to compute all combinations of elements from arrays:
```js
const adjectives = ['angry', 'boring', 'contemplative']
const nouns = ['dad', 'elephant', 'farmer']
const verbs = ['gamble', 'hibernate', 'imagine']
const sentences = []
for (const adjective of adjectives) {
for (const noun of nouns) {
for (const verb of verbs) {
sentences.push(`${adjective} ${noun} ${verb}s`)
}
}
}
sentences
/* [
'angry dad gambles',
'angry dad hibernates',
'angry dad imagines',
'angry elephant gambles',
'angry elephant hibernates',
'angry elephant imagines',
'angry farmer gambles',
'angry farmer hibernates',
'angry farmer imagines',
'boring dad gambles',
'boring dad hibernates',
'boring dad imagines',
'boring elephant gambles',
'boring elephant hibernates',
'boring elephant imagines',
'boring farmer gambles',
'boring farmer hibernates',
'boring farmer imagines',
'contemplative dad gambles',
'contemplative dad hibernates',
'contemplative dad imagines',
'contemplative elephant gambles',
'contemplative elephant hibernates',
'contemplative elephant imagines',
'contemplative farmer gambles',
'contemplative farmer hibernates',
'contemplative farmer imagines'
] */
```
### Functions
#### Anonymous functions
JavaScript has strong support for "anonymous" or "lambda" functions, i.e. functions that can be treated as values.
To get a sense of this, it may be helpful to see some examples:
```js
// "square is the function that takes x and returns x * x"
const square = x => x * x
// "call square, passing in 5 as x"
square(5) // 25
// Since square is a value, we can pass it into another function
[10, 9, 8].map(square) // [100, 81, 64]
// We can also put multiple lines of code inside functions.
// This is equivalent to the "square" defined above.
const square2 = x => {
const squared = x * x
return squared
}
const list = []
// A function that takes two arguments:
// a value and the number of times to add it to the list
const addCopiesOfValue = (value, times) => {
for (let i = 0; i < times; i++) list.push(value)
}
addCopiesOfValue('a', 2)
addCopiesOfValue('b', 0)
addCopiesOfValue('c', 3)
list // ['a', 'a', 'c', 'c', 'c']
// You don't have to store functions in variables at all;
// they can be passed directly to other functions
const sum = [1, 2, 3, 4, 5].reduce((a, b) => a + b)
sum // 15
```
#### `function`
There is a convenient shorthand for assigning names to functions:
```js
// Equivalent to `let transpose = matrix => { /* ... */ }`
function transpose(matrix) {
const result = []
for (let i = 0; i < matrix[0].length; i++) {
result[i] = []
for (let j = 0; j < matrix.length; j++) {
result[i][j] = matrix[j][i]
}
}
return result
}
transpose([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
/* [
[1, 4, 7],
[2, 5, 8],
[3, 6, 9]
] */
```
#### Parameters
JavaScript functions can be called with more or fewer arguments than they are declared with.
Missing arguments are passed as `undefined` and extra arguments are ignored.
This can be very handy or a source of tricky bugs.
For example:
```js
const arr = ['a', 'b', 'c', 'd', 'e']
// .map() expects a function which takes an array value and its index.
// If we don't care about the index, we can just use the value:
arr.map(letter => letter.toUpperCase())
// ['A', 'B', 'C', 'D', 'E']
arr.map((letter, index) => letter.repeat(index + 1))
// ['a', 'bb', 'ccc', 'dddd', 'eeeee']
// Returns the result of running f the specified number of times
function simulate(f, times) {
// Default to 100 times if not specified
if (times === undefined) times = 100
const results = []
while (times > 0) {
results.push(f())
times--
}
return results
}
simulate(Math.random)
// [0.9646134999107248, 0.6281785239945497, 0.34087991779123406, ...]
simulate(() => {
// Computes the number of dice rolls needed to get all 6 outcomes
const diceRollsSeen = []
for (let i = 0; i < 6; i++) diceRollsSeen[i] = false
let rolls = 0
while (!diceRollsSeen.every(rolled => rolled)) {
diceRollsSeen[Math.floor(Math.random() * 6)] = true
rolls++
}
return rolls
}, 10)
// [12, 25, 40, 18, 9, 14, 14, 14, 10, 22]
```
### Classes
JavaScript supports object-oriented programming much like Python or Java.
Object classes can be defined using the keyword `class`.
#### Constructor
Every class has a constructor, which is a function that can be called with the `new` keyword to create an instance of the class.
The constructor is declared inside the `class` with the notation `constructor(...arguments) { /* ... */ }`.
It can take arguments just like any other function, but it can't return anything.
If you don't provide a constructor, one that does nothing will be created automatically.
Inside the constructor, `this` refers to the new object being constructed.
You can set fields on an object by assigning a value to `this.fieldName`.
Like Python, fields don't need to be declared outside the constructor and other methods can also add fields to the object.
#### Methods
Methods are declared inside the `class` using `methodName(...args) { /* ... */ }`.
Methods work identically to normal functions, except `this` automatically refers to the object the method was called on.
You can also add `static` in front of a method to make it a method you can call on the *class* instead of on an object.
JS doesn't (currently) have a concept of private methods or fields; all fields can be accessed and all methods can be called outside the `class`.
It is common to give methods a name starting with a `_` if they are meant to be private.
#### Inheritance
Like Python or Java, JS supports object inheritance, where one type of object can be a special case of another type of object.
These "subclasses" have all the fields and methods of their "superclasses", plus additional functionality.
To define a subclass, you can write `class SubClass extends SuperClass { /* ... */ }`.
The `constructor` of a subclass needs to call the superclass's constructor, which can be done using `super(...args)`.
You can also extend builtin classes like `Array`:
```js
// A Vector3D is a 3-element Array,
// representing the x-, y-, and z-coordinates of a 3D vector
class Vector3D extends Array {
constructor(x, y, z) {
super() // call `new Array()` to make an empty array
this.push(x, y, z)
}
add(vector) {
return new Vector3D(
this[0] + vector[0],
this[1] + vector[1],
this[2] + vector[2]
)
}
multiply(scalar) {
return new Vector3D(
scalar * this[0],
scalar * this[1],
scalar * this[2]
)
}
magnitude() {
// Use Array methods to compute the magnitude
return Math.sqrt(
this.map(x => x ** 2) // square each component
.reduce((a, b) => a + b) // and add them
)
}
static zero() {
return new Vector3D(0, 0, 0)
}
}
// Vector3D [5, 7, 9]
new Vector3D(1, 2, 3).add(new Vector3D(4, 5, 6))
// Vector3D [-3, 0, 3]
new Vector3D(-1, 0, 1).multiply(3)
// 7
new Vector3D(2, 3, 6).magnitude()
// Vector3D [0, 0, 0]
Vector3D.zero()
```
#### Complete example
A `Lazy` class that computes a value only when it is needed for the first time:
```js
class Lazy {
constructor(f) {
this.f = f
}
getValue() {
if (this.value === undefined) this.value = this.f()
return this.value
}
}
const lazyRandom = new Lazy(Math.random)
lazyRandom.getValue() // 0.5037322551664427
lazyRandom.getValue() // 0.5037322551664427
```
### `Set`
JavaScript `Set`s are a builtin datatype analogous to `set` in Python and `LinkedHashSet` in Java.
They are collections that can be quickly queried to see if they contain a given value.
Here are the methods you can call on `Set`s:
```js
// Make an empty set
const words = new Set()
// Add some words to the set
words.add('zebra')
words.add('east')
words.add('running')
// Check whether a word is in the set
words.has('east') // true
words.has('west') // false
// Remove a word from the set
words.delete('zebra')
words.has('running') // true
words.has('zebra') // false
// Get the size of the set
words // Set {'east', 'running'}
words.size // 2
// Iterate over the set.
// Values are returned in the order they were added to the set.
let phrase = ''
for (const word of words) phrase += word + ' '
phrase.trim() // 'east running'
// You can also build a set that is a copy of an array or another set:
const alsoWords = new Set(['apple', 'blackboard', 'chainsaw'])
```
Sets can contain any values, both primitives and objects.
However, objects are always compared using `===`.
For example:
```js
const vectors = new Set()
vectors.add([1, 2, 3])
vectors // Set {[1, 2, 3]}
vectors.has([1, 2, 3]) // false
// If you want to check whether an *equivalent* value is in the set,
// store the values as strings instead:
const vectors2 = new Set()
vectors2.add([1, 2, 3].join(' '))
vectors2 // Set {'1 2 3'}
vectors2.has([1, 2, 3].join(' ')) // true
vectors2.has([1, 2, 4].join(' ')) // false
```
### `Map`
`Map`s in JavaScript are similar to `Set`s: they map keys to values and allow keys to be quickly looked up in the map.
They are similar to `dict`s in Python or `LinkedHashMap`s in Java.
They can be used as follows:
```js
// Make a new map, mapping numbers to their prime factors
const primeFactors = new Map()
for (let testPrime = 2; testPrime <= 100; testPrime++) {
// Check whether `testPrime` is already in the `primeFactors` map.
// If so, it must not be prime.
if (primeFactors.has(testPrime)) continue
// Add all multiples of the prime into `primeFactors`
for (let multiple = testPrime; multiple <= 100; multiple += testPrime) {
// Get `multiple`'s existing list of factors.
// If `multiple` is not in the map, `get()` returns `undefined`.
let factors = primeFactors.get(multiple)
if (factors === undefined) {
// If `multiple` wasn't in the map, create an empty list of factors for it
factors = []
primeFactors.set(multiple, factors)
}
// Add the prime factor to the list
factors.push(testPrime)
}
}
// Iterate over the keys and corresponding values in the map.
// They are returned in the order they were added to the map.
for (const [number, factors] of primeFactors) {
// "42 has factors 2, 3, 7", etc.
console.log(`${number} has factors ${factors.join(', ')}`)
}
// Other methods:
primeFactors.size // 99
primeFactors.delete(2) // deletes the key 2 from the map
```
#### Example
You can use a `Map` to store the return values of a recursive function on different inputs in order to avoid calling it again.
This technique is called "memoization".
```js
// The nth Catalan number counts the ways to match up n pairs of parentheses.
// (See https://en.wikipedia.org/wiki/Catalan_number for more use cases.)
const catalanNumbers = new Map()
function catalanNumber(n) {
// The 0th Catalan number is 1
if (n === 0) return 1
// If we've already computed the nth Catalan number, just return it
const savedResult = catalanNumbers.get(n)
if (savedResult !== undefined) return savedResult
// Otherwise, compute it and save it
let result = 0
for (let i = 0; i < n; i++) {
result += catalanNumber(i) * catalanNumber(n - 1 - i)
}
catalanNumbers.set(n, result)
return result
}
// 4861946401452
catalanNumber(25)
```
### Error handling
JavaScript allows you to throw errors just like in Python or Java.
For example, a Node.js program might throw an error if it is called with the wrong number of arguments:
```js
if (process.argv.length !== 4) {
throw new Error('Invalid syntax. Usage: node copy.js source destination')
}
const source = process.argv[2]
const destination = process.argv[3]
// Copy the file `source` to `destination`
// ...
```
Running `node copy.js source` without a second argument prints the following message and exits the program:
```
$ node copy.js source
/Users/csander/repos/cs-11-async/copy.js:2
throw new Error('Invalid syntax. Usage: node copy.js source destination')
^
Error: Invalid syntax. Usage: node copy.js source destination
at Object.<anonymous> (/Users/csander/repos/cs-11-async/copy.js:2:9)
at Module._compile (internal/modules/cjs/loader.js:1144:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1164:10)
at Module.load (internal/modules/cjs/loader.js:993:32)
at Function.Module._load (internal/modules/cjs/loader.js:892:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)
at internal/main/run_main_module.js:17:47
```
By default, throwing an error exits the function it was thrown in, which exits the function that called it, and so on, eventually terminating the program.
However, it is often useful to handle an error by running some other code.
This can be done by wrapping the code that throws the error inside a `try`-`catch` block.
For example, `changePassword()` can intercept an error thrown inside the `authenticateUser()` user function and return an appropriate response.
```js
// Never store raw passwords in a real application!
const users = [
{username: 'csander', password: 'i<3async'},
{username: 'admin', password: '123456'}
]
// Check that the given user has the given password.
// If not, throw an error.
function authenticateUser(username, password) {
const user = users.find(user => user.username === username)
if (user === undefined) throw new Error('No such user')
if (password !== user.password) throw new Error('Incorrect password')
}
function changePassword(username, password, newPassword) {
try {
authenticateUser(username, password)
}
catch (error) {
// If username/password was invalid,
// report the authentication error message to the client
return {success: false, message: error.message}
}
// Otherwise, change the user's password as requested
const user = users.find(user => user.username === username)
user.password = newPassword
return {success: true}
}
```
### Advanced function parameters
There are several additional types of function parameters:
```js
// Default arguments.
// For example, simulate() in the functions example could be rewritten:
function simulate(f, times = 100) { // default to 100 times if not specified
const results = []
while (times > 0) {
results.push(f())
times--
}
return results
}
simulate(Math.random)
// [0.8004992632182273, 0.4693706190197571, 0.6858328708181958, ...]
simulate(Math.random, 2)
// [0.9457667615017811, 0.9236581755819082]
// Destructuring arguments.
// This can be used to pass parameters with names:
const countOccurrences = ({string, search}) =>
string.split(search).length - 1
countOccurrences({search: 'ock', string: 'Ruddock rhymes with buttock'})
// 2
// Rest arguments.
// This allows a function to be called with a variable number of arguments.
// Math.max() and Math.min() already work like this:
Math.max(1, 2) // 2
Math.min(6, 5, 10, 7, 9, 8) // 5
// To get the maximum of an array, use the ... ("spread") notation:
const values = [100, 10, 1000, 10000, 1]
Math.max(...values) // 10000
// Also use the ... notation to collect the arguments into an array of arguments
function zip(...arrays) {
const result = []
while (true) {
const zipped = []
for (const array of arrays) {
if (result.length === array.length) {
return result
}
zipped.push(array[result.length])
}
result.push(zipped)
}
}
// This sets `arrays` to [[1, 2, 3], ['a', 'b', 'c']]
zip([1, 2, 3], ['a', 'b', 'c'])
// [[1, 'a'], [2, 'b'], [3, 'c']]
```
## Browser-side JavaScript
[JavaScript](https://en.wikipedia.org/wiki/JavaScript) was originally designed as a language that would run in browsers to add dynamic functionality to webpages.
JS code can, for example, access and modify the current web page, handle clicks and other events, and make requests to other websites.
Writing programs in JS is appealing because everyone has a JS interpreter (namely, a web browser) installed on their computer.
Although different browsers and versions of the same browser support slightly different sets of JS features, the core language functionality is identical across browsers.
### Browser-side example
Web browsers render HTML documents.
HTML is *not* a programming language; it is a "Markup Language" that can be used to render text, tables, links, forms, etc.
We won't dive too deeply into how HTML works, but it consists of several types of "elements" that can be nested inside each other.
For example, `<button> button content </button>` makes a `button` element with `button content` displayed inside it.
You can include JavaScript code in an HTML document by putting it inside a `<script>` tag.
[`calculator.html`](calculator.html) is an example of an HTML document that defines some elements and some JS code that interacts with them.
If you download it and open it in any modern web browser, you should see a calculator that you can interact with.
### Browser-side documentation
Mozilla's MDN has great documentation and examples of the entire [language](https://developer.mozilla.org/en-US/docs/Web/JavaScript).
Its pages on JavaScript's builtin objects (e.g. [`Array.push()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push)) also have extensive examples of how to use them.
## Node.js
More recently, programmers have started writing JavaScript that runs *outside* a browser.
This is called "server-side" JS, as opposed to "client-side" or "browser-side" JS.
To run server-side JS code, you will need to install the [Node.js](https://nodejs.org/) runtime.
Both client-side and server-side JavaScript are designed around asynchronous APIs, so some projects in this course will run in the browser and some will run in Node.js.
The server-side JS language is **identical** to client-side JS; the only difference is in the set of standard-library functions each provides.
### Running the interpreter
Running Node.js scripts looks a lot more like running Python scripts than running JS in the browser.
To execute the JS file `script.js`, you would run `node script.js` in a terminal.
Node.js also has a REPL that you can open by running `node`, which is very useful for testing out bits of code.
### Modules
Node.js has a powerful module system that allows you to define `function`s, `class`es, etc. in one file and use them in another.
Unlike in Python modules, variables and functions defined in one file are not visible outside it by default; you need to *explicitly* export them from the file.
Each module exports whatever value it assigns to `module.exports`.
For example, we can make a module `linked-list.js` that exports a `LinkedList` class and a function `arrayToList`:
```js
class LinkedList {
constructor(node) {
this.head = node
}
addFront(data) {
return new LinkedList({data, rest: this.head})
}
toArray() {
const values = []
for (let current = this.head; current; current = current.rest) {
values.push(current.data)
}
return values
}
}
function arrayToList(array) {
let list = new LinkedList()
for (let i = array.length - 1; i >= 0; i--) {
list = list.addFront(array[i])
}
return list
}
// We export an *object* with `LinkedList` and `arrayToList` as its fields
module.exports = {
LinkedList,
arrayToList
}
```
In Node.js, you call the function `require()` to import a module.
The location of the module to import is passed to `require()`.
For example, if `linked-list.js` is in the same directory, you would call `require('./linked-list')`.
If `linked-list.js` were inside the directory `utils`, you would call `require('./utils/linked-list')`.
```js
// It is common to "unpack" the values exported from another module
const {arrayToList, LinkedList} = require('./linked-list')
const empty = new LinkedList()
empty.toArray() // []
const list234 = arrayToList([2, 3, 4])
const list1234 = list234.addFront(1)
list1234.toArray() // [1, 2, 3, 4]
list234.toArray() // [2, 3, 4]
```
Most of the functions included in Node.js need to be imported from the corresponding library, e.g. `fs` for file system operations, `http` for making HTTP servers and requests, and `zlib` for compressing and decompressing files.
See the example below for using one of these modules.
[npm packages](#installing-npm-packages) are also imported the same way.
### Node.js example
[`copy.js`](copy.js) is a simple Node.js program that copies one file to another (like the command-line program `cp`).
If you download it, you can run it as `node copy.js source_file destination_file`.
It reports an error if the wrong number of arguments are passed or an error is thrown when reading the source file or writing the destination file.
```js
// Import the file system module
const fs = require('fs')
if (process.argv.length !== 4) {
throw new Error('Invalid syntax. Usage: node copy.js source destination')
}
// Extract the command line arguments
const source = process.argv[2]
const destination = process.argv[3]
// Copy `source` to `destination`.
// The notes on streams discuss a more efficient way to do this.
// Read the contents of `source`
fs.readFile(source, (err, data) => {
if (err) throw err
// Write the contents to `destination`
fs.writeFile(destination, data, err => {
if (err) throw err
})
})
```
### Installing npm packages
If you haven't used a package manager before, the idea is simple: they allow people to publish open source code libraries for other projects to use.
Node.js's package manager is called [npm](npmjs.com) and it hosts more published packages than any other package manager.
If you want to use npm with a new project, run `npm init` to create a `package.json` file: this file stores information about the project, including which packages it depends on.
However, we will provide you all the `package.json` files you need.
To install the "dependencies" specified in `package.json`, run `npm install`.
For example, the [`colors`](https://www.npmjs.com/package/colors) package on npm allows you to print colored text in the terminal.
You could use the following `package.json` for a new project `colors-test` that depends on version 1.4.0 or newer of `colors`:
```json
{
"name": "colors-test",
"version": "0.0.1",
"dependencies": {
"colors": "^1.4.0"
}
}
```
Running `npm install` installs `colors` and prints:
```
added 1 package from 2 contributors and audited 1 package in 0.323s
found 0 vulnerabilities
```
Now we can `require('colors')` in our program:
```js
const colors = require('colors')
console.log(
colors.red('r') +
colors.yellow('y') +
colors.green('g') +
colors.blue('b')
)
```
Running this program prints:
![Colors test](colors.png)
### Node.js documentation
Node.js also has very detailed documentation of its standard library at [nodejs.org](https://nodejs.org/api/).
It's broken down by module (e.g. [`fs`](https://nodejs.org/api/fs.html) or [`http`](https://nodejs.org/api/http.html)) with entries for all the functions exported by each module (e.g. [`fs.readFile()`](https://nodejs.org/api/fs.html#fs_fs_readfile_path_options_callback)).
const fs = require('fs').promises
let cachedContents
function getContents() {
// If we have already read the file, just return its contents
if (cachedContents !== undefined) {
return Promise.resolve(cachedContents)
}
// Otherwise, read the file and store its contents
const readPromise = fs.readFile('file.txt', 'utf8')
readPromise.then(contents => {
cachedContents = contents
})
return readPromise
}
const start1 = process.hrtime()
getContents().then(_ => {
// Reading file took 1946343 ns
const time1 = process.hrtime(start1)
console.log(`Reading file took ${time1[1]} ns`)
const start2 = process.hrtime()
getContents().then(_ => {
// Using cached file took 29273 ns
const time2 = process.hrtime(start2)
console.log(`Using cached file took ${time2[1]} ns`)
})
})
const fs = require('fs').promises
fs.readFile('a.txt', 'utf8')
.then(aContents => {
// If the contents of a.txt don't end with a newline character,
// reject with an error
if (!aContents.endsWith('\n')) {
throw new Error('Missing newline at end of a.txt')
}
// Otherwise, read b.txt and concatenate the files' contents
return fs.readFile('b.txt', 'utf8')
.then(bContents => Promise.resolve(aContents + bContents))
})
// If either file didn't exist or a.txt didn't end with a newline,
// resolve to the empty string instead
.catch(err => Promise.resolve(''))
// Finally, write the string to c.txt
.then(concatenated => fs.writeFile('c.txt', concatenated))
/*
Note the `.promises`.
`require('fs').promises` has the same functions as `require('fs')`,
but they return Promises instead of taking callbacks.
*/
const fs = require('fs').promises
// Read 'a.txt'. `readPromise` is a `Promise<string>`.
const readPromise = fs.readFile('a.txt', 'utf8')
/*
After `readPromise` finishes, write the result to 'b.txt'.
`contents` will be the `string` that was read.
We return a `Promise<undefined>` that represents the write.
*/
const writePromise = readPromise
.then(contents => fs.writeFile('b.txt', contents))
// Once the write finishes, print a message
writePromise.then(_ => {
console.log('Copying done')
})
const fs = require('fs').promises
const aPromise = fs.readFile('a.txt', 'utf8')
aPromise.then(contents => fs.writeFile('b.txt', contents))
aPromise.then(contents => fs.writeFile('c.txt', contents))
const fs = require('fs')
// The arguments are the file to create and its initial contents.
// Run the program using node create-file.js FILENAME CONTENTS
fs.stat(process.argv[2], err => {
if (err) {
// File didn't exist, so create it
fs.writeFile(process.argv[2], process.argv[3], err => {
if (err) throw err
console.log(`${process.argv[2]} created`)
})
}
else console.log(`${process.argv[2]} already exists`)
})
# Promises
## Passing data to callbacks
In the previous set of notes, we used callbacks as a way to run some code *when* something happens.
However, we often want to know *what* happened.
In this case, we can pass the result of an asychronous action as an argument to the callback function.
We'll look this week at some of the asynchronous functions that make up Node.js's filesystem API.
The full set of functions can be found in the [documentation for the `fs` module](https://nodejs.org/api/fs.html).
There are quite a lot, so we'll look at four of the most useful ones:
- [`fs.readFile()`](https://nodejs.org/api/fs.html#fs_fs_readfile_path_options_callback), used to read the entire contents of a file
- [`fs.writeFile()`](https://nodejs.org/api/fs.html#fs_fs_writefile_file_data_options_callback), used to set the entire contents of a file
- [`fs.readdir()`](https://nodejs.org/api/fs.html#fs_fs_readdir_path_options_callback), used to list the files in a directory
- [`fs.stat()`](https://nodejs.org/api/fs.html#fs_fs_stat_path_options_callback), used to get metadata about a file (e.g. its size, when it was last modified, etc.)
To illustrate the first two of these, [here](write-echo.js) is an example program that writes to a file and then reads its contents:
```js
// Import Node.js's 'fs' module
const fs = require('fs')
// Write 'hello, world' to the file
fs.writeFile('something.txt', 'hello, world', (err) => {
if (err) throw err
// Now read the file; it should contain 'hello, world' now.
// 'utf8' indicates that the file is a text file encoded in UTF-8.
fs.readFile('something.txt', 'utf8', (err, data) => {
if (err) throw err
// Prints 'hello, world'
console.log(data)
})
})
```
## Callback hell — nesting
You may have noticed that the asynchronous programs above require significantly more code than their synchronous counterparts.
For example, we would write something like this instead in a synchronous language:
```js
writeFile('something.txt', 'hello, world')
console.log(readFile('something.txt', 'utf8'))
```
The general observation that using callbacks tends to make programs more complicated is called "callback hell".
One aspect of callback hell is that, as we saw last time, asynchronous tasks that happen *in sequence* must be nested inside the callbacks of the previous tasks.
For example, [here](size-sum.js) is a program that computes the total size of 3 files:
```js
fs.stat('a.txt', (err, statsA) => {
if (err) throw err
fs.stat('b.txt', (err, statsB) => {
if (err) throw err
fs.stat('c.txt', (err, statsC) => {
if (err) throw err
const totalSize = statsA.size + statsB.size + statsC.size
console.log(`Total size: ${totalSize} bytes`)
})
})
})
```
## Callback hell — error handling
Error handling in asynchronous code is tricky.
Does the following code catch the error that is thrown inside the `setTimeout()` callback?
```js
try {
setTimeout(() => {
throw new Error('Error from timeout')
}, 1000)
}
catch (e) {
console.error(`Error was caught: ${e}`)
}
```
The answer is no.
Remember that `setTimeout()` returns as soon as the callback has been set up, without waiting for the 1 second to elapse.
Therefore, the `try` block ends before the error is thrown.
So how can we "catch" an error that occurs during an asynchronous action?
As you can see in the examples in the previous sections, all of Node.js's asynchronous functions pass an `err` as the first parameter to the callback.
If an error occurred, `err` is set to the `Error` object that was "thrown".
Otherwise, `err` is set to `null`.
For example, `fs.stat()` gives an error if the file we try to read doesn't exist.
We can use this to create a file if it doesn't exist:
```js
// The arguments are the file to create and its initial contents.
// Run the program using node create-file.js FILENAME CONTENTS
fs.stat(process.argv[2], err => {
if (err) {
// File didn't exist, so create it
fs.writeFile(process.argv[2], process.argv[3], err => {
if (err) throw err
console.log(`${process.argv[2]} created`)
})
}
else console.log(`${process.argv[2]} already exists`)
})
```
If we run [this program](create-file.js) using `node create-file.js new-file.txt abc`, it will make the file `new-file.txt` with contents `abc`.
If we run it again with `node create-file.js new-file.txt def`, it won't change the file because it already exists.
But most of the time, we don't want any special error handling.
If an error happens in a sequence of asynchronous actions, we usually want to stop and report an error from the whole sequence.
As you can see in the examples above, writing `if (err) throw err` for every easynchronous action gets quite tedious.
## What is a `Promise`?
In JavaScript, a `Promise` represents an asynchronous task that "promises" to compute a value eventually.
We write `Promise<T>` to denote a `Promise` that will compute a value of type `T`.
This may sound very abstract, so here are some concrete examples:
- Reading a file's contents can be represented as a `Promise<string>`
- Looking up a user in a database can be represented as a `Promise<User>`
- An asynchronous task that *doesn't* compute a value can be represented as a `Promise<undefined>`
It is important to keep in mind that a `Promise<T>` is different from a `T`.
**There is no way to "get" the `T` from a `Promise<T>`**.
When a `Promise` computes a value, this is called "resolving" to the value.
All `Promise`s can also "reject" with an error.
## `Promise` methods
### `.then()`
This method is used to run `Promise`s in sequence.
Because running asynchronous tasks in sequence is extremely common, `.then()` is the primary way to compose `Promise`s.
[Here](copy-promises.js) is how it is used:
```js
/*
Note the `.promises`.
`require('fs').promises` has the same functions as `require('fs')`,
but they return Promises instead of taking callbacks.
*/
const fs = require('fs').promises
// Read 'a.txt'. `readPromise` is a `Promise<string>`.
const readPromise = fs.readFile('a.txt', 'utf8')
/*
After `readPromise` finishes, write the result to 'b.txt'.
`contents` will be the `string` that was read.
We return a `Promise<undefined>` that represents the write.
*/
const writePromise = readPromise
.then(contents => fs.writeFile('b.txt', contents))
// Once the write finishes, print a message
writePromise.then(_ => {
console.log('Copying done')
})
```
In general, if we have a `Promise<A>` and a function `A => Promise<B>`, we can use `.then()` to chain them together into a single `Promise<B>`.
You can call `.then()` multiples times on the same `Promise` if multiple actions need to happen after the `Promise` finishes.
For [example](copy-twice.js), to copy `a.txt` to both `b.txt` and `c.txt`:
```js
const aPromise = fs.readFile('a.txt', 'utf8')
aPromise.then(contents => fs.writeFile('b.txt', contents))
aPromise.then(contents => fs.writeFile('c.txt', contents))
```
### `Promise.resolve()`
This function is used to make a `Promise` that immediately resolves to a given value.
This is useful for a function which sometimes performs an asynchronous action but sometimes returns a pre-computed value.
For [example](cache.js), we can save the contents of a file after reading it the first time:
```js
let cachedContents
function getContents() {
// If we have already read the file, just return its contents
if (cachedContents !== undefined) {
return Promise.resolve(cachedContents)
}
// Otherwise, read the file and store its contents
const readPromise = fs.readFile('file.txt', 'utf8')
readPromise.then(contents => {
cachedContents = contents
})
return readPromise
}
```
### `.catch()`
Error handling with `Promise`s is much easier than with callbacks since `.then()` automatically handles errors that occur.
In [this example](catch-file.js), we concatenate the contents of two files.
If `a.txt` doesn't exist, then `fs.readFile()` will reject and the `aContents => ...` function will never be called.
If `a.txt` doesn't end with a newline, we throw an error, which will also cause the `Promise` to reject.
The `err => ...` function will be called if the `Promise` rejects for any reason.
It reurns a new `Promise` to run instead.
Note that the `concatenated => ...` function will always be called because we catch any error that could have occurred.
```js
fs.readFile('a.txt', 'utf8')
.then(aContents => {
// If the contents of a.txt don't end with a newline character,
// reject with an error
if (!aContents.endsWith('\n')) {
throw new Error('Missing newline at end of a.txt')
}
// Otherwise, read b.txt and concatenate the files' contents
return fs.readFile('b.txt', 'utf8')
.then(bContents => Promise.resolve(aContents + bContents))
})
// If either file didn't exist or a.txt didn't end with a newline,
// resolve to the empty string instead
.catch(err => Promise.resolve(''))
// Finally, write the string to c.txt
.then(concatenated => fs.writeFile('c.txt', concatenated))
```
### `Promise.all()`
`Promise.all()` takes an array of `Promise`s and returns a new `Promise` that resolves once *all* of the `Promise`s have resolved.
When it resolves, it computes an array containing the result of each of the `Promise`s.
In [this example](recursive-mtime.js), we find the most recent modification time of all files inside all subdirectories of a given directory.
(For example, if `a/b/c.txt` was modified, that counts as a modification of `a/b` and `a` as well.)
```js
// Returns a Promise<number> that computes the most recent
// modification time for any file inside a directory
const modifiedTime = path =>
fs.stat(path).then(stats => {
// mtimeMs is the modification time for this file/directory
const {mtimeMs} = stats
if (stats.isDirectory()) {
// If this is a directory, compute the modification time
// for all files/subdirectories inside it
return fs.readdir(path)
.then(files =>
// Compute modification times of all contained files
Promise.all(files.map(file =>
modifiedTime(path + '/' + file)
))
)
// modifiedTimes will be an array containing the latest
// modification time of each file in the directory
.then(modifiedTimes => Math.max(mtimeMs, ...modifiedTimes))
}
// If this is a file, just return its modification time
else return Promise.resolve(mtimeMs)
})
```
## *Aside*: monads
If you've ever used Haskell, you might have noticed that `Promise`s look a lot like its `IO` monad.
`.then()` is the equivalent of `>>=` ("bind"), which chains two IO operations together.
And `Promise.resolve()` is the equivalent of `return`, which wraps a normal value in an instance of `IO`.
Just like monads in Haskell allow you to write code that looks imperative in a functional language, `Promise`s let you write asynchronous programs that look like blocking programs, while still using callbacks under the hood.
Towards the end of the course, we'll also cover JavaScript's `async`-`await` notation, which hides even the calls to `.then()`.
This is very similar to `do` notation in Haskell.
const fs = require('fs').promises
// Returns a Promise<number> that computes the most recent
// modification time for any file inside a directory
const modifiedTime = path =>
fs.stat(path).then(stats => {
// mtimeMs is the modification time for this file/directory
const {mtimeMs} = stats
if (stats.isDirectory()) {
// If this is a directory, compute the modification time
// for all files/subdirectories inside it
return fs.readdir(path)
.then(files =>
// Compute modification times of all contained files
Promise.all(files.map(file =>
modifiedTime(path + '/' + file)
))
)
// modifiedTimes will be an array containing the latest
// modification time of each file in the directory
.then(modifiedTimes => Math.max(mtimeMs, ...modifiedTimes))
}
// If this is a file, just return its modification time
else return Promise.resolve(mtimeMs)
})
modifiedTime('.').then(mTime => {
console.log(`Most recent modification: ${new Date(mTime)}`)
})
const fs = require('fs')
fs.writeFile('a.txt', 'some text', err => {
if (err) throw err
fs.writeFile('b.txt', 'some more text', err => {
if (err) throw err
fs.writeFile('c.txt', 'even more text', err => {
if (err) throw err
fs.stat('a.txt', (err, statsA) => {
if (err) throw err
fs.stat('b.txt', (err, statsB) => {
if (err) throw err
fs.stat('c.txt', (err, statsC) => {
if (err) throw err
const totalSize = statsA.size + statsB.size + statsC.size
console.log(`Total size: ${totalSize} bytes`)
})
})
})
})
})
})
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment