Functional programming

like Erlang but in Javascript!

Created by Guido D'Orsi / @fughye

I Am

Guido D'Orsi

Retro games lover

Frontend web developer @ ImmobiliareLabs

Agenda:

How to improve our Javascript modules with a functional approach.

  • First class functions
  • Pure functions
  • Currying
  • Composition

Our case study

Google maps geocoding

Google Maps Geocoding

							
function Geocoder(input, output, button) {
    var _this = this;

    this.input = input;
    this.output = output;
    this.geocoder = new google.maps.Geocoder();

    button.addEventListener('click', function () {
        _this.geocodeAddress();
    });
}
							
						

Google Maps Geocoding

							
Geocoder.prototype.geocodeAddress = function () {
    var _this = this;

    this.geocoder.geocode({
        address: this.input.value
    }, function (results, status) {
        _this.handleGeocodeResult(results, status);
    });
};
							
						

Google Maps Geocoding

							
Geocoder.prototype.handleGeocodeResult = function (results, status) {
    var location = {},
        components, i, l;

    if (results && results.length > 0 && status == google.maps.GeocoderStatus.OK) {
        components = results[0]['address_components'];

        for(i = 0, l = addressComponents.length; i < l; i++){
            switch(addressComponents[i].types[0]){
                case 'administrative_area_level_2':
                    location.province = components[i].short_name;
                    break;
                ...
                case 'street_number':
                    location.streetNumber = components[i].long_name;
                    break;
            }
        }
        this.locationData = location;
        this.showAddress();
    } else {
        alert('Insert a valid address');
    }
};
							
						

Google Maps Geocoding

							
Geocoder.prototype.showAddress = function () {
    var formattedData = this.locationData.province + ', ' +
        this.locationData.city + ', ' + this.locationData.route;

    if(this.locationData.streetNumber){
        formattedData +=  ', ' + this.locationData.streetNumber;
    }

    this.output.textContent = formattedData;
};
							
						

First class functions

Let's fly in first class!

					    
iNeedACallback(firstClass);
					    
					

Array slice

              
function iNeedArgs() {
  return Array.prototype.slice.call(arguments, 1);
}
			  
			  
function firstClassSlice(array, from, to) {
  return Array.prototype.slice.call(array, from, to);
}

function iNeedArgs() {
  return firstClassSlice(arguments, 1);
}
              
            

Pure happiness with pure functions

Pure functions

“A pure function is a function that, given the same input, will always return the same output and does not have any observable side effect.”

Professor Fisby's mostly adequate
guide to funcitonal programming.

Side effects may include, but are not limited to:

  • Mutations on objects
  • Make http calls
  • Write logs
  • Query the dom
              
//impure
var goodEnaugh = 6;

function doYouLikeThisTip(vote) {
  return vote >= goodEnaugh && 'Yes' || 'No';
}
              
            
						
function doYouLikeThisTip(voto) {
  var goodEnaugh = 6;

  return voto >= goodEnaugh && 'Si' || 'No';
}


var config = Object.freeze({
  goodEnaugh: 6
});

function doYouLikeThisTip(v) {
  return vote >= config.goodEnaugh && 'Yes' || 'No';
}
						
					

Let's bring some purity to our module

							
Geocoder.prototype.showAddress = function () {
  var formattedData = this.locationData.province + ', ' +
  this.locationData.city + ', ' + this.locationData.route;

  if(this.locationData.streetNumber){
    formattedData += ', ' + this.locationData.streetNumber;
  }

  this.output.textContent = formattedData;
}
							
						

How about splitting the responsibilities?

							
//Transform locationData in a formatted string
function formatAddress(locationData) {}

//Show the formatted string in the output element
function showAddress(output, formattedData) {}
							
						

Now we can write a pure function

							
function formatAddress(locationData) {
  var formattedData = locationData.province + ', ' +
  locationData.city + ', ' + locationData.route;

  if(locationData.streetNumber){
    formattedData += ', ' + locationData.streetNumber;
  }

  return formattedData;
}

							
						

And isolate the 'impure' code

							
function showAddress(output, formattedData) {
  output.textContent = formattedData;
}
							
						

But if we want...

							
function showAddress(output, formattedData) {
  return function() {
    output.textContent = formattedData;
  };
}
							
						

Currying

							
curry(function add(a, b) {
  return a + b;
})

//Partial
var increment = add(1);

//Execution
increment(10);
//11
							
						

Currying can be done with a curry function...

						
var curry = require('lodash/curry');

var join =  curry(function (glue, array) {
  return array && array.join(glue);
});

var joinWithComma = join(', ');

joinWithComma(['RM', 'Rome']);
//Rm, Rome
						
					

...or with bind!

						
function join(glue, array) {
  return array && array.join(glue);
};

var joinWithComma = join.bind(null, ', ');

joinWithComma(['RM', 'Rome']);
//Rm, Rome
						
					

Coming back to our module...

							
function formatAddress(locationData) {
  var formattedData = locationData.province + ', ' +
    locationData.city + ', ' + locationData.route;

  if(this.locationData.streetNumber){
    formattedData += ', ' + locationData.streetNumber;
  }

  return formattedData;
}

							
						

To be more functional, we use list join instead of string concat

							
function locationDataToArray(locationData){
  return locationData && [
    locationData.province,
    locationData.city,
    locationData.route,
    locationData.streetNumber
  ] || [];
}

function formatAddress(locationData) {
  return joinWithComma(
    locationDataToArray(locationData)
  );
}
							
						

And a filter function to clean the dirt

							
var filter = curry(function (predicate, array) {
  return array && array.filter(predicate);
});

function isTruthy(value) {
  return !!value;
}

var filterInvalid = filter(isTruthy);

function formatAddress(locationData) {
  return joinWithComma(
    filterInvalid(
      locationDataToArray(locationData)
    )
  );
}
							
						

Coding by composing

							
function compose(f, g) {
  return function(x) {
    return f(g(x));
  };
};
            
          

Composed version is better!

							
function formatAddress (locationData) {
  return joinWithComma(
    filterInvalid(
      locationDataToArray(locationData)
    )
  );
}
							
							
var compose = require('lodash/flowRight');

var formatAddress = compose(
  join(', '),
  filter(isTruthy),
  locationDataToArray
);
            
          

And now convert the rest!

							
Geocoder.prototype.handleGeocodeResult = function (results, status) {
  var locationData = {},
    addressComponents, i, l;

if (results && results.length > 0 && status == google.maps.GeocoderStatus.OK) {
    addressComponents = results[0]['address_components'];

    for(i = 0, l = addressComponents.length; i < l; i++){
      switch(addressComponents[i].types[0]){
        case 'administrative_area_level_2':
          locationData.province = addressComponents[i].short_name;
          break;
        ...
        case 'street_number':
          locationData.streetNumber = addressComponents[i].long_name;
          break;
      }
    }
    this.locationData = locationData;
    this.showAddress();
  } else {
    alert('Insert a valid address');
  }
};
							
						

Status check!

							
function validGeocodeResponse(results, status) {
  return status == google.maps.GeocoderStatus.OK && results;
}
							
						

Reduce the data!

							
var reduce = require('lodash/collection/reduce');

function transformAddressComponents(addressComponents) {
  return addressComponents && reduce(addressComponents, function(res, component) {
    switch(component.types[0]){
      case 'administrative_area_level_2':
        res.province = component.short_name;
        break;
      ...
      case 'street_number':
        res.streetNumber = component.long_name;
        break;
    }
    return res;
  }, {}) || {};
}
							
						

And like composing a melody...

							

var getProperty = curry(function (propName, obj) {
  return obj && obj[propName];
});

var first = getProperty(0);
							
							
var formatGeocodeResponse = compose(
  formatAddress,
  transformAddressComponents,
  getProperty('address_components'),
  first,
  validGeocodeResponse
);
							
						

...even if not everything is pure...

							
var showAddress = curry(function(errorMessage, output, formattedData) {
  if(formattedData) {
      output.textContent = formattedData;
  } else {
      alert(errorMessage);
  }
});
              
						

...Our functional geocoding...

							
var geocode = curry(function (geocoder, output, address) {
  geocoder.geocode({
        address: address
  }, compose(
    showAddress('Insert a valid address', output),
    formatGeocodeResponse
  ));
})
              
						

...IS HERE!

							
function manageAddressGeocoding(input, output, button){
  var geocodeOnOutput = geocode(
     new google.maps.Geocoder(),
     output
  );
  button.addEventListener('click', function() {
    geocodeOnOutput(input.value);
  });
}
              
						

The time is over, but there are so many other things!

  • Memoizing
  • Immutability
  • Functors

Questions?