Functional programming – like Erlang but in Javascript! – Our case study



Functional programming – like Erlang but in Javascript! – Our case study

0 0


talks


On Github fughye / talks

Functional programming

like Erlang but in Javascript!

Created by Guido D'Orsi / @fughye

I Am

Guido D'Orsi

Front-end web developer @ Immobiliare.it

Retro games lover

Sviluppare software senza attenersi a dei principi di design porta sempre a risultati scadenti sotto tutti i punti di vista. Costi di produzione, tempi di sviluppo, User Experience, poca affidabilità del codice, poca manuntenibiltà. Con questo breve talk non intendo mostrarvi che sono stato illuminato dal signore e l'unica strada giusta è programmare abbandonando completamente l'approccio Object-Oriented o procedurale e passando a un funzionale puro. La mia intenzione è di portarvi quella che è la mia esperienza nell'applicare principi di programmazione funzionale nel mio lavoro quotidiano e i benefici che questo comporta. Inoltre è mia intenzione quella di portare prossimamente qui un talk su Redux, una libreria davvero molto semplice e interessante, che serve a gestire lo stato delle interfacce e che si basa molto sui concetti della programmazione funzionale.

Oggi vi parlerò di:

Programmazione funzionale con Javascript!

  • First class functions
  • Pure functions
  • Currying
  • Composition

Il nostro caso d'uso

Google maps geocoding

Immaginiamo di dover sviluppare un componente che dato un indirizzo inserito dall'utente ne verifica l'esistenza e mostra il risultato geolocalizzato formattato con:

Provincia, Città, Indirizzo, Numero civico

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 'administrative_area_level_3':
                    location.city = components[i].long_name;
                    break;
                case 'route':
                    location.route = components[i].long_name;
                    break;
                case 'street_number':
                    location.streetNumber = components[i].long_name;
                    break;
            }
        }
        this.locationData = location;
        this.showAddress();
    } else {
        alert('Inserisci un indirizzo valido!');
    }
};
						

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;
};
						

Google Maps Geocoding

First class functions

Anche le funzioni possono viaggiare in prima classe!

					    iNeedACallback(firstClass);
					
Passiamo adesso ad un pò di teoria. Come in algebra, se le funzioni rispettano determinate condizioni, hanno tante belle propietà che ci permettono di fare le nostre fighettate. Iniziamo con le funzioni di prima classe.

Array slice

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

//First class
function firstClassSlice(array, from, to) {
  return Array.prototype.slice.call(array, from, to);
}

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

Pure functions

La felicità di sentirsi puri!

Si dice funzone pura, una funzione che dato un determinato input, ritorna sempre lo stesso output e non ha nessun side-effect osservabile. In pratica una funzione che ad ogni chiamata è sempre vergine.

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.
Si dice funzone pura, una funzione che dato un determinato input, ritorna sempre lo stesso output e non ha nessun side-effect osservabile. In pratica una funzione che ad ogni chiamata è sempre vergine.

Esempi di side effects:

  • Mutazioni di oggetti
  • Effettuare chiamate http
  • Scrivere dei log
  • Lanciare una query sul DOM
  • Altro ancora...
Si dice funzone pura, una funzione che dato un determinato input, ritorna sempre lo stesso output. E quand'è che una funzione non rispetta questa condizione? Quando il risultato dipende da uno stato esterno.
              //impure
var soglia = 6;

function tePiaceQuestaTip(voto) {
  return voto >= soglia && 'Si' || 'No';
}
            
In questo caso invece la prima funzione è impura, in quanto dipende da una variabile esterna, che può cambiare nel corso del tempo influendo quindi sul risultato della funzione
						function tePiaceQuestaTip(voto) {
  var soglia = 6;

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

//oppure
var config = Object.freeze({
  soglia: 6
});

function tePiaceQuestaTip(voto) {
  return voto >= config.soglia && 'Si' || 'No';
}
					
In questo caso invece la prima funzione è impura, in quanto dipende da una variabile esterna, che può cambiare nel corso del tempo influendo quindi sul risultato della funzione

Proviamo a portare un pò di purezza nel nostro modulo

							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;
}
						

Dividiamo un pò le responsabilità

							//Trasforma locationData in una stringa formattata
function formatAddress(locationData) {}

//Mostra la stringa formattata nel nostro elemento di output
function showAddress(output, formattedData) {}
						
							function formatAddress(locationData) {
  var formattedData = locationData.province + ', ' +
  locationData.city + ', ' + locationData.route;

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

  return formattedData;
}
						

Almeno abbiamo circoscritto il codice 'impuro'

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

Se proprio vogliamo...

							//Qui manteniamo ancora la purezza
function showAddress(output, formattedData) {
  //Anche se qualcuno il lavoro sporco lo deve ancora fare
  return function() {
    output.textContent = formattedData;
  };
}
						
Che ci abbiamo guadagnato? showAddress prima aveva due responsabilità, era scocciante da testare con riusabilità zero. La nuova versione si testa facilmente, non è legata al nostro vecchio oggetto e possiamo farci quello che ci pare (caching).

Il vantaggio che abbiamo è chiaro:

  • Prevedibilità
  • Riusabilità
  • Testabilità

Currying

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

//Partial
var increment = add(1);

//Execution
increment(10);
//11
						
Il concetto di currying è semplice. Puoi passare una funzione meno parametri di quanti ne aspetta. Questa ritornerà una nuova funzione che prenderà in input i restanti parametri.

Currying con lodash

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

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

var joinWithComma = join(', ');

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

Currying con bind

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

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

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

Torniamo al nostro esempio

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

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

  return formattedData;
}
						

E passiamo dalla concatenzaione al join

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

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

Creiamo un filtro

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

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

var filterInvalid = filter(isTruthy);
						

E lo applichiamo alla lista

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

Coding by composing

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

Meglio no?

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

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

E adesso concludiamo

							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 'administrative_area_level_3':
          locationData.city = addressComponents[i].long_name;
          break;
        case 'route':
          locationData.route = addressComponents[i].long_name;
          break;
        case 'street_number':
          locationData.streetNumber = addressComponents[i].long_name;
          break;
      }
    }
    this.locationData = locationData;
    this.showAddress();
  } else {
    alert('Inserisci un indirizzo valido!');
  }
};
						

Usiamo curry e composition per il check e l'estrazione dei dati

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

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

var first = getProperty(0);

var getAddressComponents = compose(
  getProperty('address_components'),
  first,
  validGeocodeResponse
);
						

Riduciamo!

							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 'administrative_area_level_3':
        res.city = component.long_name;				<li></li>

        break;
      case 'route':
        res.route = component.long_name;
        break;
      case 'street_number':
        res.streetNumber = component.long_name;
        break;
    }
    return res;
  }, {}) || {};
}
						

Componiamo

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

E anche se non è tutto puro

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

Il nostro geocoding

							var geocode = curry(function (geocoder, output, address) {
  geocoder.geocode({
        address: address
  }, compose(
    showAddress('Inserisci un indirizzo valido!', output),
    formatGeocodeResponse
  ));
})
						

Funziona, è manutenibile, è sicuro ed è testabile!

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

Ma quante cose ci sono di cui non vi ho parlato!

  • Memoizing
  • Immutability
  • Functors
Functional programming like Erlang but in Javascript! Created by Guido D'Orsi / @fughye