JavaScript Arrays

part II

http://slides.com/tuxsudo/javascript-arrays/live#/

"YOU ONLY REALLY NEED TO KNOW 4 METHODS TO BE SUPER EFFECIENT [SIC]:

 

forEach, map, filter, reduce"

-- Cory Brown

"ARMED WITH THESE TOOLS, YOU ARE NOW PREPARED TO ENTER THE WORLD OF FUNCTIONAL PROGRAMING. [sic] "

-- Cory Brown

KNOWS WAY TOO MUCH ABOUT EVERYTHING

NEVER HEARD OF SPELCHECKR

Refresher

[].forEach()

Execute

[].map()

Transform

[].filter()

Um, Filter

[].reduce()

Calculate

execute against each item

transform each item

get rid of some items

reduce to a single, calculated value

[].forEach
function(item, i, original)
[].map
[].filter
function(previous, item, i, original)
[].reduce
undefined
[]
[]
//anything
function(item, i, original)
function(item, i, original)
//anything
boolean
//anything

method

returns

expects callback

callback should return

are optional

i
original

&

and usually excluded

Two More, just for fun

[].every()

Test All

[].some()

Test for 1

all items must pass

at least 1 item must pass

[].every
[].some
boolean
boolean
function(item, i, original)
boolean
boolean
function(item, i, original)

method

returns

expects callback

callback returns

[].forEach
function(item, i, original)
[].map
[].filter
function(previous, item, i, original)
[].reduce
undefined
[]
[]
//anything
function(item, i, original)
function(item, i, original)
//anything
boolean
//anything

method

returns

expects callback

callback returns

[].every
[].some
boolean
boolean
function(item, i, original)
boolean
boolean
function(item, i, original)

are optional

i
original

&

and usually excluded

Some Examples

var favs = ['pizza', 'cheeseburgers', 'bacon'],
    results;


function addToPage(food) {
    var div = document.createElement('div');
    div.innerHTML = food;
    document.body.appendChild( div );
}

results = favs.forEach(addToPage);

console.log(results);

[].forEach

var favs = ['pizza', 'cheeseburgers', 'veggies', 'bacon'],
    results;


function critique(item, i, original) {

    if(item==="veggies") {
        return "No rabbit food for me, thanks!"
    } else {
        return item + " is good";
    }

}

results = favs.map(critique);


console.log(results);

[].map

[].map

var dimensions = [
    {w:10,h:20,d:10},
    {w:3,h:2,d:20},
    {w:4,h:1,d:400},
    {w:9000,h:3999,d:9191}
];

function area(dimension) {
    return dimension.w * dimension.h;
}


function volume(dimension) {
    return dimension.w * dimension.h * dimension.d;
}

[].filter, [].some, [].every

var favs = ['pizza', 'peas', 'cheeseburgers',
    'carrots', 'bacon', 'spinach'];

function all(item) {
    return false;
}

function none(item) {
    return true;
}

function veggie(item) {
    return /peas|carrots|spinach/.test(item);
}

function noVeggie(item) {
    return !/peas|carrots|spinach/.test(item);
}

[].reduce

More on [].reduce

[].reduce( function(previous, item, i, original), unshiftVal);
function sum(previous, item) {
    return previous + item;
}


balance = payments.reduce(sum, loan);
var loan = -800,
    payments = [100.44, 111.32, 44.11, 54.33, 12.11],
    balance;

[-800, 100.44, 111.32, 44.11, 54.33, 12.11]

1. "unshift"

unshiftVal

(conceptually)

[].reduce( function(previous, item, i, original), unshiftVal);
// loan = -800
// payments = [100.44, 111.32, 44.11, 54.33, 12.11]
// payments.reduce(sum, loan);

+


[-800, 100.44, 111.32, 44.11, 54.33, 12.11]

-800

-699.56

+   111.32

-588.24

+    44.11

-544.13

+   54.33

-489.80

+  12.11

-477.69

function sum(previous, item) {
    return previous + item;
}

+100.44

var favs = [
    ['pizza', 'italian', 'mexican', 'veggies'],
    ['italian', 'mexican', 'noodles'],
    ['italian', 'chinese',  'pizza'],
    ['cheeseburgers', 'bacon', 'fruit'],
    ['salad', 'mushrooms', 'mexican'],
    ['salad', 'bacon', 'fruit'],
    ['sugar', 'chinese',  'pizza'],
    ['pizza']
];

function tallyFavs(previous, item) {
    item.forEach( function addTally(food) {
    	previous[food] = previous[food] && previous[food]+1 || 1;
    });

    return previous;
}

[].reduce

favs.reduce( tallyFavs, {} );
var weights = [150, 140, 220, 300, 690, 844, 230, 12, 43];


function toAvg(last, current, i, original) {

    var total = last + current;

    return i+1 < original.length ? total : total / original.length;

}

[].reduce

weights.reduce( toAvg );

Now

the fun part

Friend Schema

{
    "name": "Jane",
    "age": 30,
    "gender": "female",
    "weight": 140,
    "married": true,
    "kids": 4,

    "location": {
        "lat": 40.511841544251176,
        "lon": -111.98006585240364
    },

    "favs": {
        "foods": [],
        "movies": [],
        "books": []
    },

    "tags": []

}
var friends = friendService.findFriends();

// Giant Array of Friends

Some Functions

// Some Filters
function hasKids(item) {
    return item.kids && item.kids > 0;
}

function eatsPizza (item) {
    return item.favs.food.indexOf('pizza') >= 0;
}

// Some Maps (accepts full person object, returns only their location)
function attributeLocations (item) {
    return item.location;
}

function attributeAges (item) {
    return item.age;
}


// A Reduction
function toAverage (last, item, i, original) {
    var sum = last + item;
    return i+1<original.length ? sum : sum/original.length;
}

// An executable (forEach)
function logName(item) {
    console.log(item.name);
}
friends.forEach(logName);
friends.filter(hasKids).forEach(logName);
friends
    .filter(hasKids)
    .filter(eatsPizza)
    .forEach(logName);
friends.filter(hasKids).map(attributeAges);

Demo Time

Observations:

  • functions are reusable and can be composed in many orders
  • pretty powerful for only a few functions
  • we're just getting started..

G  O  I  N  G

D  E  E  P  E  R

A Higher Order

// Some Filters
function eatsPizza (item) {
    var foods = item.favs.food;
    return foods.indexOf('pizza') >= 0;
}

function eatsItalian (item) {
    var foods = item.favs.food;
    return foods.indexOf('italian') >= 0;
}

function eatsMexican
function eatsChinese
function eatsWhatever

// --> to infinity?! F T L
function eats(food) {
    return function(item) {
	return item.favs.food.indexOf(food) >= 0;
    };
}

// then:
friends.filter( eats('pizza') );
friends.filter( eats('chinese') );
friends.filter( eats('vegetables') );

// --> Infinity F  T  W

This can go on forever. Write a new function

every time someone adds a new favorite

type of food?

function eats(food) {
    return function(item) {
	return item.favs.food.indexOf(food) >= 0;
    };
}
[].filter
boolean function(item, i, original)

expects callback

Remember?!:

AND:

JavaScript functions can create and return other functions

function eatsPizza (item) {
    var foods = item.favs.food;
    return foods.indexOf('pizza') >= 0;
}

like:

like:

function eats(food) {
    return function(item) {
	return item.favs.food.indexOf(food) >= 0;
    };
}

And in this case, the "signature" of the returned function matches the "signature" of what [].filter expects for its callback

eg:

function(item) {
    return item.favs.food.indexOf('pizza') >= 0;
}
eats('pizza')
function(item) {
    return item.favs.food.indexOf('chinese') >= 0;
}
eats('chinese')
function(item) {
    return item.favs.food.indexOf('vegetables') >= 0;
}
eats('vegetables')

which is plopped into [].filter(

)

etc.

function attributeLocations (item) {
	return item.location;
}
function attributeAges (item) {
	return item.age;
}

// --> Sadly to infinity

same for our [].map functions

function toAttribute(attribute) {
    return function(item) {
	return item[attribute];
    }
}

// then:
friends.map(toAttribute('age'));
friends.map(toAttribute('location'));

// --> Happily to infinity

Putting it all together

function eats(food) {
    return function(item) {
        return item.favs.food.indexOf(food) >= 0;
    };
}
function has(attribute) {
    return function(item) {
	return item[attribute] && true || false;
    }
}
function min(attribute, value) {
    return function(item) {
	return item[attribute] >= value;
    }
}
function max(attribute, value) {
    return function(item) {
	return item[attribute] <= value;
    }
}
function withTag(tag) {
    return function(item) {
	return item.tags.indexOf(tag) >= 0;
    };
}

More Better Filters

friends.filter(eats('pizza'));
friends.filter(has('distance'));
friends.filter(min('age', 35));
friends.filter(max('kids', 5));
friends.filter(withTag('bff'));
function negate(fn) {
    return function(item) {
    	return !fn(item);
  };
}
friends.filter(negate(eats('pizza')));

Which friends don't eat pizza?

function dontEats(food) {
    return function(item) {
        return item.favs.food.indexOf(food) < 0;
    };
}
friends.filter(dontEats('pizza'));

Negate ALL THE THINGS!!

friends.filter(negate(withTag('bff')));
function toAttribute(attribute) {
    return function(item) {
	return item[attribute];
    }
}

More Better Map

friends.map(toAttribute('name'));
function setAttribute(attribute, value) {
    return function(item) {
        var cloned = clone(item);
        cloned[attribute] = value;
	return cloned;
    }
}
friends.map(
    setAttribute('iterated', true)
);
function addTag(tag) {
    return function(item) {
        var cloned = clone(item);
        cloned.tags.push(tag);
	return cloned;
    }
}
friends.map(addTag("bff"));
function removeTag(tag) {
    return function(item) {
        var cloned = clone(item),
            tags = cloned.tags.filter(function(t){
                return t !== tag;
            });
        cloned.tags = tags;
	return cloned;
    }
}
friends.map(removeTag("bff"));
friends
    .filter( eats('pizza') )
    .filter( has('kids') )
    .map( addTag('invited') )
    .forEach( persist );


friends
    .filter( eats('veggies') )
    .filter( hasTag('bff') )
    .map( removeTag('bff') )
    .forEach( prankCall );


friends
    .filter(negate(has('gender')))
    .map(setAttribute('name', 'Pat'))
    .map(addTag( 'shady' ))
    .forEach(persist);


So Expressive, No?

Function.bind

speaking of functions returning functions

function.prototype.bind()?!

function makeLovers(person1, person2) {
    var lover1 = person1,
    	lover2 = person2;

    return lover1 + " & " + lover2 + " sittin' in a tree";
}
function.prototype.bind(thisArg, argument1, argument2);
function

returns

makeLovers("Jared", "Sara");
//Jared & Sara sittin' in a tree
var jaredLoves = makeLovers.bind(null, "Jared");
// function
var jaredLoves = function (person2) {
    var lover1 = "Jared",
    	lover2 = person2;

    return lover1 + " & " + lover2 + " sittin' in a tree";
}

==

jaredLoves("Sara");
//Jared & Sara sittin' in a tree
function setDistance(person1, person2) {
    var loc1 = person1.location,
    	loc2 = person2.location,
        cloned = clone(person2), // pass through a cloning function to keep pure
        radlat1 = Math.PI * loc1.lat/180,
        radlat2 = Math.PI * loc2.lat/180,
        radlon1 = Math.PI * loc1.lon/180,
        radlon2 = Math.PI * loc2.lon/180,

        theta = loc1.lon-loc2.lon,
        radtheta = Math.PI * theta/180,

    dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
    dist = Math.acos(dist);
    dist = dist * 180/Math.PI;
    dist = dist * 60 * 1.1515;
    cloned.distance = (dist * 0.8684)
    return cloned;
}
friends.map( setDistance.bind(null, me) );

[].map + fn.bind = <3

function alike(person, attribute) {
    return function(item) {
        return person[attribute]===item[attribute];
    }

}
friends.filter( alike(me, 'gender') );

+ another "relative" filter

The End