Monthly Archives: December 2013

Ninjatriks i JavaScript, del 2

Jeg var ikke sikker på om det kom til å skje, men når jeg øynet muligheten til å kombinere mine to favoritthobbyer, koding og øl, måtte jeg jo bare. Dermed blir det del 2 av ninjatriks i JavaScript, denne gang med fokus på funksjonell programmering og underscore.js.

Ifølge Douglas Crockford er JavaScript “Lisp in C’s Clothing”. Dvs, det er et funksjonelt språk utkledd som et prosedyrespråk. De fleste som har skrevet litt JavaScript har funnet ut at det går fint å skrive prosedyre (og kanskje også objekt)-orientert i JavaScript.

Men, hvis det nå er sånn at JavaScript er funksjonelt så burde det kanskje være noe å hente? Jeg snakker ikke helt nazi no-sideeffects, everything is a function, men noen gode ideer her og der?

Det har ihvertfall jeg funnet ut de senere år, etter at jeg tok i bruk Backbone.js og dermed Underscore.js. Underscore.js er et lite utility-bibliotek som beskriver seg som “It’s the tie to go along with jQuery’s tux, and Backbone.js’s suspenders.”. Ja, mange av de funksjonene underscore.js tilbyr finnes som “native”-javascript funksjoner i nyere implementasjoner. Dog, jeg har lært meg underscore og trives med det, dermed så vil jeg fokusere på det.

Men, hva er det å hente? Hva gir underscore og en funksjonell tankegang meg? Jeg tenkte å ta noen eksempler, så kan du se selv.

Eksempel 1 er ganske greit. Du har ei liste med objekter (la oss si øl-objekter). Du vil finne alle objekter (dvs øl) som er produsert av Haandbryggeriet. Gitt lista (som tilfeldigvis er fra en juleølsmaking jeg deltok på):

    var beers = [
       {"brewery": "Austmann", "name": "Jingle Beer", "score": 6.49, "abv": 7},
        {"brewery": "Binding-Brauerei", "name": "Santa Clausthaler", "score": 2.14, "abv": 0},
        {"brewery": "Brasserie St-Feuillien", "name": "Cuvée de Noël", "score": 6.43, "abv": 9},
        {"brewery": "Dahls", "name": "Julebrygg", "score": 5.19, "abv": 4.5},
        {"brewery": "Fanø Bryghus", "name": "Julebryg", "score": 5.74, "abv": 7.2},
        {"brewery": "Grans Bryggeri", "name": "Lade Gaards Brygghus Juleøl", "score": 5.92, "abv": 6.5},
        {"brewery": "Hansa", "name": "Ekstra Vellagret Julebrygg", "score": 6.39, "abv": 6.5},
        {"brewery": "Haandbryggeriet", "name": "Nissefar", "score": 6.07, "abv": 7},
        {"brewery": "Haandbryggeriet", "name": "Nissegodt", "score": 6.26, "abv": 4.5},
        {"brewery": "Inderøy", "name": "Nisseøl", "score": 6.06, "abv": 4.5},
        {"brewery": "Nøgne Ø", "name": "God Jul", "score": 5.89, "abv": 8.5},
        {"brewery": "Nøgne Ø", "name": "Julesnadder", "score": 7.19, "abv": 4.5},
        {"brewery": "Nøgne Ø", "name": "Special Holiday Ale", "score": 6.71, "abv": 9},
        {"brewery": "Ægir", "name": "Julebrygg", "score": 6.21, "abv": 4.7}
    ];

Hvis vi nå vil finne ut hvilke som var butikkøl (dvs abv <= 4.7) kan du gjøre dette med en for-løkke:

    var pol_limit = 4.7;
    var shop_beers = [];
    for (var i = 0; i< beers.length; i++) {
        var beer = beers[i];
        if (beer.abv <= pol_limit) {
            shop_beers.push(beer);
        }
    }
    console.log(shop_beers);

Ganske standard, men mye boilerplate og unødvendige temporær-variabler. Hva med Underscore.js? Vi kan bruke _.filter():

var isShopBeer = function (beer) {
    return (beer.abv <= 4.7);
};
console.log(_.filter(beers, isShopBeer));

Det som skjer her er at _.filter() looper gjennom lista beers og returnerer en ny liste over de ølene som returnerer true i funksjonen isShopBeer. Ganske fiffig, færre linjer og lettere utbyttbart.

En annen artig ting, er at siden vi nå har en funksjon for å bestemme om et øl er butikkøl, vet vi at alle øl som returnerer false i denne er poløl. Dermed kan vi finne antall poløl ved å bruke _.reject():

console.log(_.reject(beers, isShopBeer));

Så, dermed kan vi finne ut at vi hadde:

var shopBeers = _.filter(beers, isShopBeer);
var polBeers = _.reject(beers, isShopBeer);

console.log("Antall øl: " + beers.length);
console.log("Antall butikkøl: " + shopBeers.length);
console.log("Antall poløl: " + polBeers.length);

eller:

  • Antall øl: 14
  • Antall butikkøl: 6
  • Antall poløl: 8

Hva om vi vil finne ut hvilket øl som gjorde det best? Og dårligst? Med en for-løkke kunne vi gjort dette som

var max = beers[0];
for(var i = 1; i max.score) {
        max = beer;
    }
}
console.log(max.name) //Julesnadder

Med underscore kan vi bruke _.max():

var getBeerScore = function(beer) {
    return beer.score;
}

console.log(_.max(beers, getBeerScore)); //Julesnadder

og tilsvarende har vi såklart _.min():

console.log(_.min(beers, getBeerScore).name); //Santa Clausthaler

Altså:

var best = _.max(beers, getBeerScore);
var worst = _.min(beers, getBeerScore);

console.log("Best: ", best.brewery + " " + best.name + " (" + best.score + "/10)");
console.log("Værst: ", worst.brewery + " " + worst.name + " (" + worst.score + "/10)");

eller:

  • Best: Nøgne Ø Julesnadder (7.19/10)
  • Værst: Binding-Brauerei Santa Clausthaler (2.14/10)

Best av polølene da? Typisk ville vi gjort noe sånt som:

var pol_limit = 4.7;
    var max = 0.0;
    var best;
    for (var i = 0; i< beers.length; i++) {
        var beer = beers[i];
        if (beer.abv > pol_limit && beer.score > max) {
            console.log(beer.name)
            best = beer;
            max = beer.score;
        }
    }
    console.log(best.name); //Special Holiday Ale

Men, vi kan kombinere underscore-funksjoner:

console.log(_.max(_.reject(beers, isShopBeer), getBeerScore)); //Special Holiday Ale

Hvor mange øl var det med fra hvert bryggeri?

Vel, en måte å løse det på er følgende:

var result = {};
for (var i = 0; i< beers.length; i++) {
    var beer = beers[i];
    if (result.hasOwnProperty(beer.brewery)) {
        result[beer.brewery] += 1;
    } else {
            result[beer.brewery] = 1;
    }
} 
console.log(result);

Med underscore kan vi bruke _.groupBy():

var getBrewery = function (beer) {
    return beer.brewery;
}
console.log(_.groupBy(beers, getBrewery))

denne gir oss riktignok en liste med øl-objekter på hver key, ikke bare antallet. Det kan vi dog få til med en _.reduce():

console.log(_.reduce(_.groupBy(beers, getBrewery), function (memo, beers, brewery) {
    memo[brewery] = beers.length;
    return memo;
},{}));

Men, det spørs om det er det vi egentlig vil? Med resultatet av groupBy'en kan vi spytte ut endel statistikk:

_.each(_.groupBy(beers, getBrewery), function (beers, brewery) {
    var avg_score = _.reduce(beers, function(sum, beer){ return sum + beer.score; }, 0) / beers.length;
    console.log(brewery + ": " + beers.length + " øl, " + avg_score + " i gj.snitt");
});

Vel, det var det for denne gang. Håper du lærte noe nytt, hvis du har spørsmål eller kommentarer er det bare å mase (jeg har sikkert gjort noen feil, jeg er på ingen måte noen mester, men jeg lærer stadig!)