Memoization: metti il turbo alle tue funzioni Javascript

La memoizzazione – da non confondere con memorizzazione – è una tecnica usata allo scopo di evitare che una stessa funzione, onerosa dal punto di vista computazionale, venga eseguita più volte con gli stessi valori. In pratica si fa in modo che il valore di ritorno corrispondente a certi parametri finisca in una sorta di cache, cosicché eventuali successive chiamate alla stessa funzione con gli stessi parametri non comportino delle nuove esecuzioni della funzione stessa.
In javascript questo simpatico giochetto si può implementare in un paio di modi, a seconda che la funzione accetti parametri oppure no.

Funzione senza parametri, caso più “semplice”

Ipotizziamo che nel nostro programma da qualche parte ci sia una funzione che non si aspetti alcun parametro in ingresso e che durante l’esecuzione del programma potrebbe dover essere eseguita più volte. In questo caso – non molto frequente ma nemmeno raro – mettere il risultato in una “cache” non ha proprio senso perché non si ha nemmeno una “chiave” con cui riandarlo a pescare. Si possono quindi sfruttare gli scope e il fatto che una funzione è un oggetto come tutti gli altri (e quindi memorizzabile in una variabile).

// funzione originale
var getSomething = function(){
  var something;
  something = {}; // logica onerosa
  return something;
};

// funzione modificata
var getSomethingMem = function(){
  var something;

  something = {}; // logica onerosa

  getSomethingMem = function(){
    return something;
  };
  return getSomethingMem();
};

Si ha una funzione anonima getSomething che esegue tutta una serie di calcoli onerosi e ritorna un risultato. Per farne il memoize è sufficiente – una volta eseguita la computazione e salvato il risultato in una variabile – assegnare alla variabile contenente questa funzione una nuova funzione che ritorni direttamente il valore calcolato. La variabile something è inclusa nello scope di questa nuova funzione “fasulla” getSomethingMem, e questa funzione dopo la prima esecuzione non farà altro che ritornare il valore calcolato la prima volta.

Funzione con parametri, la cache

Il “trucco” illustrato sopra non si può applicare al caso di funzione che si aspetta dei parametri in ingresso, ma la flessibilità del javascript ci permette di assegnare una cache a qualunque funzione.
Spiegarla a parole non è proprio semplice lascio quindi a voi il piacere di capirla, in fondo sono solo poche righe di codice.
In questo caso la “cache” è un modulo CommonJs, la sua funzione create prende in ingresso una funzione (ad esempio una list) e ritorna un oggetto avente una sua implementazione “estesa” con la cache della stessa funzione list, più eventuali altre funzioni di utilità.

var list = function(params){
  // logica onerosa, ma stateless e indipendente dal tempo
  return {};
};

list = require('Cache').create(list).get;
exports.create = function(f, params){
  params = params || {};
  
  var cache, get, reset;
  
  cache = {};
  
  get = function(){
    var key = JSON.stringify(Array.prototype.slice.call(arguments));
    if (key in cache){
      console.log('From cache: ' + key);
      return cache[key];
    } else {
      console.log('Not in cache: ' + key);
      return cache[key] = f.apply(this, arguments);
    }
  };
  
  reset = function(){
    cache = {};
  };
  
  return {
    'get' : get,
    'reset' : reset
  };
};

Detto che la funzione di memoize può essere implementata in vari modi (quella sopra è una versione modificata di una che trovai da qualche parte sul web), è chiaro che si possono trovare librerie che ci alleggeriscono del compito. Librerie -> Javascript -> ovviamente qualcosa c’è anche in Underscore.js.

In sintesi

È la memoization la cura per ogni male? Chiaramente no. Può essere usata sempre? No, la risposta è quasi sempre no quando in una domanda compaiono dei quantificatori universali come “ogni” o “sempre”…
Queste tecniche potrebbero risolvervi grossi problemi di prestazioni ad esempio in presenza di funzioni lente perché causa di molte letture su disco, a discapito di un maggior utilizzo della memoria, ma questo divario si riduce di molto se i dischi sono SSD.
In presenza di funzioni stateful (il male, almeno quando si usano linguaggi funzionali), chiaramente non si può pensare di bypassare l’esecuzione della funzione andando a pescare il valore in una cache, così come non si può fare troppo affidamento su una cache se i risultati cambiano allo scorrere del tempo (ad esempio perché le elaborazioni sono basate su dati aggiornati periodicamente).
Il caso limite è quello di una funzione molto complessa che fa molte query su un database i cui dati cambiano ma non troppo spesso. In un caso del genere il suo utilizzo ci potrebbe stare ma bisognerebbe implementare adeguate logiche di “pulizia” della cache dei risultati a ogni aggiornamento dei dati.

A usarla bene il vostro smartphone/tablet potrebbe anche spiccare il volo, ma facendolo con leggerezza i risultati delle vostre funzioni saranno sbagliati… “Da un grande potere derivano grandi responsabilità”.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.