On Github fughye / talks
Created by Guido D'Orsi / @fughye
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.Programmazione funzionale con Javascript!
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
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(); }); }
Geocoder.prototype.geocodeAddress = function () { var _this = this; this.geocoder.geocode({ address: this.input.value }, function (results, status) { _this.handleGeocodeResult(results, status); }); };
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!'); } };
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; };
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.
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); }
“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.”
//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
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; }
//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; }
function showAddress(output, formattedData) { output.textContent = formattedData; }
//Qui manteniamo ancora la purezza function showAddress(output, formattedData) { //Anche se qualcuno il lavoro sporco lo deve ancora fare return function() { output.textContent = formattedData; }; }
curry(function add(a, b) { return a + b; }) //Partial var increment = add(1); //Execution increment(10); //11Il 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.
var curry = require('lodash/curry'); var join = curry(function (glue, array) { return array && array.join(glue); }); var joinWithComma = join(', '); joinWithComma(['RM', 'Rome']); //Rm, Rome
function join(glue, array) { return array && array.join(glue); }; var joinWithComma = join.bind(null, ', '); joinWithComma(['RM', 'Rome']); //Rm, Rome
function formatAddress(locationData) { var formattedData = locationData.province + ', ' + locationData.city + ', ' + locationData.route; if(this.locationData.streetNumber){ formattedData += ', ' + locationData.streetNumber; } return formattedData; }
function locationDataToArray(locationData){ return locationData && [ locationData.province, locationData.city, locationData.route, locationData.streetNumber ] || []; } function formatAddress(locationData) { return joinWithComma( locationDataToArray(locationData) ); }
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) ) ); }
function compose(f, g) { return function(x) { return f(g(x)); }; };
function formatAddress (locationData) { return joinWithComma( filterInvalid( locationDataToArray(locationData) ) ); } var formatAddress = compose( join(', '), filter(isTruthy), locationDataToArray );
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!'); } };
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 );
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; }, {}) || {}; }
var formatGeocodeResponse = compose( formatAddress, transformAddressComponents, getProperty('address_components'), first, validGeocodeResponse );
var showAddress = curry(function(errorMessage, output, formattedData) { if(formattedData) { output.textContent = formattedData; } else { alert(errorMessage); } });
var geocode = curry(function (geocoder, output, address) { geocoder.geocode({ address: address }, compose( showAddress('Inserisci un indirizzo valido!', output), formatGeocodeResponse )); })
function manageAddressGeocoding(input, output, button){ var geocodeOnOutput = geocode( new google.maps.Geocoder(), output ); button.addEventListener('click', function() { geocodeOnOutput(input.value); }); }