beyond the for
loop
@tmcw / Tom MacWright / Mapbox
this is a topic that’s close to my heart because it’s fundamental, difficult, and subtle: something as simple as a loop can expressed in vastly different ways. knowing which route to take for every situation will help you write straightforward code that reflects your ideas.
loops are one of the bits of fundamental magic in programming
if { } else { }
computers can
make decisions
for (;;) { }
computers can
do work
sparknotes
arrays
arrays
are collections of data
var numbers = [1, 2, 3];
arrays have a length property
numbers.length; // 3
arrays have methods
numbers.push(4);
// puts 4 into numbers
arrays contain data
var four = numbers[3];
problem one: make some animals rock
var animals = ['cats', 'dogs'];
our desire
['cats rock', 'dogs rock'];
using for
var animals = ['cats', 'dogs'];
for (var i = 0; i < animals.length; i++) {
animals[i] = animals[i] + ' rock';
}
for
is a way of saying do this repeatedly
for is a classic example of the concept of 'imperative programming':
a style that emphasizes telling computers what to do. there are other
styles, like declarative programming, which is telling computers
what you want - a big example of that is SQL.
equivalent to while() { }
and do { } while()
and even goto
for
loops have three basic elements
where we usually make the loop counter
for (var i = 0; i < animals.length; i++) {
where we usually check if we're done
for (var i = 0; i < animals.length; i++) {
where we usually increment the loop counter
for (var i = 0; i < animals.length; i++) {
the loop is counting from 0 to 1
is that what i'm doing?
in | out |
cats
dogs |
cats rock
dogs rock |
i'm not counting from 0 to 1, i'm transforming data
function theyRock(creatures) {
return creatures + ' rock';
}
theyRock('cats'); // cats rock
function theyRock(creatures) {
return creatures + ' rock';
}
// feed ['cats', 'dogs'] into theyRock
daring thesis: we should do that
let's meet the family
map, reduce, filter
there are others but these are the hits
some of the others are .some(), which tells you if any element
of an array passes a test, .every(), which says if ever element does
they live
on arrays
[].filter; // there it is
for IE8 and lower use es5-shim
and weep legacy tears
some people use underscore or lodash for this. that's fine too,
but it's best to use tools that do exactly what you want: the mission
of underscore is to bring other functional programming goodies
to javascript, not to serve as a compatibility layer.
so what do they do?
map
creates a new array by calling a function on every element of an old array
that thing from earlier but with map
var animals = ['cats', 'dogs'];
function theyRock(creatures) {
return creatures + ' rock';
}
var rockinAnimals = animals.map(theyRock);
this says what it does: we are turning an
array of animals into an array or rockin animals with
a named function
it also does not change the original array,
but creates a new one
aka 'mutation', changing arrays in place breeds confusion
var animals = ['cats', 'dogs'];
function makeEmRock(animals) {
for (var i = 0; i < animals.length; i++)
animals[i] = animals[i] + ' rock';
return animals;
}
var rockinAnimals = makeEmRock(animals);
// rockinAnimals = [cats rock, dogs rock]
// animals = [cats rock, dogs rock] NOOOOOOO
var animals = ['cats', 'dogs'];
function makeEmRock(animals) {
return animals.map(function(animal) {
return animal + ' rock';
});
}
var rockinAnimals = makeEmRock(animals);
// rockinAnimals = [cats rock, dogs rock]
// animals = [cats, dogs]
filter
create a new array with only the elements that
pass a test
var animals = ['cats', 'dogs'];
var catsOnly = [];
for (var i = 0; i < animals.length; i++)
if (animals[i] === 'cats')
catsOnly.push(animals[i]);
// catsOnly = ['cats']
but we'd want to abstract the comparison because of those
cats that look like dogs because of their crazy cat show owners
doing weird haircuts or grooming or whatever
not judging, keep rocking cat show people
this is a function that returns true or false.
comparisons are values, liberate them from
if
statements!
function thatsACat(animal) {
return animal === 'cats';
}
var animals = ['cats', 'dogs'];
var catsOnly = [];
function thatsACat(animal) {
return animal === 'cats';
}
for (var i = 0; i < animals.length; i++)
if (thatsACat(animals[i]))
catsOnly.push(animals[i]);
// catsOnly = ['cats']
filter to the rescue
var animals = ['cats', 'dogs'];
function thatsACat(animal) {
return animal === 'cats';
}
var catsOnly = animals.filter(thatsACat);
// catsOnly = ['cats']
like map, filter creates a new array and doesn't mess with your old one
this concept, called 'immutability', is really deep. see immutable-js
for an example of it taken to the extreme. the benefits of immutability,
besides simpler code, extend into performance and the ability to
undo changes to data.
[questions]
map
array ⇢ array of transformed values
filter
array ⇢ array of filtered values
one last kind of loop: aggregation
array ⇢ sum
reduce
given an array and a starting value, get a result by calling a function
with each element of the array and the value.
um, what?
reduce is more general than the others.
let's look at it two ways.
compute the sum of an array of numbers
var numbers = [4, 8, 15, 16, 23];
var sum = 0;
for (var i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
// sum = 66
the gist: start off with a sum of 0, add each number to it
1. starting value = 0
2. add each number to current sum
3. voila!
here's the same thing with reduce!
var numbers = [4, 8, 15, 16, 23];
var sum = numbers.reduce(function(currentSum, value) {
return currentSum + value;
}, 0);
first notice the
starting value of 0,
given as the second argument
var numbers = [4, 8, 15, 16, 23];
var sum = numbers.reduce(function(currentSum, value) {
return currentSum + value;
}, 0);
this enters the function as
currentSum
var numbers = [4, 8, 15, 16, 23];
var sum = numbers.reduce(function(currentSum, value) {
return currentSum + value;
}, 0);
each loop returns a new value that is the new
currentSum
var numbers = [4, 8, 15, 16, 23];
var sum = numbers.reduce(function(currentSum, value) {
return currentSum + value;
}, 0);
so, for the first iteration it looks like
var numbers = [4, 8, 15, 16, 23];
var sum = numbers.reduce(function(currentSum=0, value=4) {
return 0 + 4;
}, 0);
and then the second
var numbers = [4, 8, 15, 16, 23];
var sum = numbers.reduce(function(currentSum=4, value=8) {
return 4 + 8;
}, 0);
and third
var numbers = [4, 8, 15, 16, 23];
var sum = numbers.reduce(function(currentSum=12, value=15) {
return 12 + 15;
}, 0);
here's another way of putting it
reduce is worth learning.
you may know reduce as fold
or foldl
from other languages.
it can do all the other ones
reduce's initial value doesn't have to be a number.
what if it's an array?
remember this?
var animals = ['cats', 'dogs'];
function theyRock(list, creatures) {
list.push(creatures + ' rock');
return list;
}
var rockinAnimals = animals.reduce(theyRock, []);
and this?
var animals = ['cats', 'dogs'];
function thatsACat(list, animal) {
if (animal === 'cats') {
list.push(animal);
}
return list;
}
var catsOnly = animals.reduce(thatsACat, []);
we can even write a general 'filter' function
using reduce.
function filter(array, fn) {
return array.reduce(function(memo, value) {
return fn(value) ?
memo.concat([value]) : memo;
}, []);
}
so, use reduce always?
for what it's worth, array.splice() is basically the reduce of
mutable array functions: you can use it instead of shift, unshift,
pop, push, and slice. but nobody ever remembers the order
of arguments and the name sounds weird so they don't.
no
use map for transforming values
use filter for filtering values
use reduce for aggregating values
use for
loops for performance hotspots
make sure they're hotspots first.
[questions]
chaining
made famous by $
$('div')
.css('background', 'Gainsboro')
.slideIn()
.doBarrelRoll();
this is a real color
$('div')
.css('background', 'Gainsboro')
.slideIn()
.doBarrelRoll();
it's grayish
anyway: chaining is useful for doing multiple
steps quickly without intermediate variables
thing
.function()
.function()
.function();
map
and
filter
return arrays
and
are functions on arrays
var rockin = ['cats', 'dogs']
.map(theyRock);
var catsOnly = ['cats', 'dogs']
.filter(thatsACat);
THEREFORE
var rockinCatsOnly = ['cats', 'dogs']
.filter(thatsACat)
.map(theyRock);
// ['cats rock']
the things that used to be in loops are now reusable
and combinable
var rockinCatsOnly = ['cats', 'dogs']
.filter(thatsACat)
.map(theyRock);
// ['cats rock']
let's talk blocks vs functions real quick
for loops are one kind of block:
for (var i = 0; i < 3; i++) {
}
blocks are in for loops,
switch statements, if / else
when you see { }
but no function
,
you're looking at a block.
blocks are chunks of work: the code within them might run lots of times
for (var i = 0; i < 3; i++) {
alert('annoy!')
}
or it might never run
if (false) {
alert('sad.')
}
blocks and functions both have { }
but have a big difference: scope
scope is containingness
variables are contained in scopes
this saves us from conflict.
without scope, every time you named a variable
you'd have to be sure it was the only thing
named that everywhere. that's crazy.
for instance,
function divide(a, b) {
var divided = a / b;
divided only exists here!
return divide;
}
var myResult = divide(1, 3);
divided doesnt exist here!
so variables are contained by scopes
and only functions give you scopes
but, Tom, we were just talking about
loops, why does this matter?
for
loops are blocks,
map, filter, reduce
are functions.
example of disaster
var numbers = [1,2,3];
var sum = 0;
// i will leave my
// important business here, yes
var importantBusiness = 42;
// copy & pasted this - me
for (var i = 0; i < numbers.length; i++) {
sum = i += numbers[i];
var importantBusiness = sum + 1;
}
importantBusiness is now equal to 6!?
o no!
variables leak out & in for
loops.
another variable leaked out of that one:
var numbers = [1,2,3];
var sum = 0;
// i will leave my
// important business here, yes
var importantBusiness = 42;
// copy & pasted this - me
for (var i = 0; i < numbers.length; i++) {
sum = i += numbers[i];
var importantBusiness = sum + 1;
}
// i = 16
now see the functional version
// i will leave my
// important business here, yes
var importantBusiness = 42;
var numbers = [1,2,3];
var sum = numbers.reduce(function(current, value) {
var importantBusiness = current + 1;
return current + value;
}, 0);
importantBusiness = 42; still! phew.
leak-free! the variables current and value
are contained within the function, and when we talk about
importantBusiness, it refers to the variable in function
scope, so the other one isn't affected.
having scopes also fixes this problem:
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 10);
}
logs 3, 3, 3
voila!
[0, 1, 2].map(function(i) {
setTimeout(function() {
console.log(i);
}, 10);
});
logs 1, 2, 3
the value is in the function's scope:
it doesn't change after being passed to the function.
[0, 1, 2].map(function(i) {
setTimeout(function() {
console.log(i);
}, 10);
});
logs 1, 2, 3