Oltre il PHP, parte 1

Monday, 23 October 06
Fino a quando lo spazio mentale di un programmatore coincide con il PHP
per lui il PHP e' infinito.

io, in chat con un amico hacker


Come promesso ecco il primo post che tenta di andare oltre i limiti del PHP... per quanto sia possibile. Iniziamo da un problema pratico ed utile nel caso tipico di applicazioni web scritte in PHP che utilizzano un database SQL: la memoization.




Nei linguaggi di programmazione imperativi come il PHP le funzioni si possono dividere in due sotto classi.

Le funzioni senza side effect sono quelle funzioni che non cambieranno in alcun modo lo stato del programma come effetto collaterale del fatto che sono state chiamate. In pratica una funzione senza side effects: Si potrebbe continuare all'infinto... in breve nessun tipo di alterazione dello stato del programma e' concesso ad una funzione senza side effects, inclusa la produzione di output (ad esempio tramite una chiamata PHP echo()).

Le funzioni con side effect sono invece tutte quelle che alterano lo stato del programma quando vengono eseguite.




Funzioni trasparenti referenzialmente

Oltre a non avere side effect una funzione puo' essere anche trasparente referenzialmente. Una funzione che ha la caratteristica della trasparenza referenziale deve sempre restituire lo stesso valore di output quando viene chiamata con lo stesso valore di input.

Di solito le funzioni trasparenti referenzialmente devono:

La seguente funzione e' sia priva di side effects che trasparente referenzialmente:

function somma($a,$b) {
    return $a+$b;
}


La funzione somma non modifica in alcun modo lo stato del programma e ritornera' sempre lo stesso valore se viene chiamata con gli stessi argomenti. Potrei prendere un programma PHP qualunque, aggiungere delle chiamate alla funzione somma in mezzo al codice in piu' parti ed essere sicuro che il programma continuerebbe a girare in maniera corretta.

Potrei anche prendere un programma che usa la funzione somma, e sostituire tuttle le chiamante alla funzione con il valore di ritorno che avrebbe la funzione chiamata con gli argomenti specificati, e anche in questo caso il programma verrebbe eseguito esattamente allo stesso modo.

Le funzioni che non hanno side effect e sono anche trasparenti referenzialmente hanno un nome, si chiamano funzioni pure


Cosa ci facciamo di bello con le funzioni pure? La memoization!




La memoization

Le funzioni pure hanno dei grandi vantaggi dal punto di vista della chiarezza del codice, della robustezza e della struttura del programma, ma per l'aspetto che stiamo investigando in questo post il vantaggio chiave e' che dal punto di vista del programma e' perfettamente uguale se il risultato che ritornano per ogni dato input, ovvero l'output della funzione, viene memorizzato dopo la prima chiamata e riutilizzato ogni altra volta che e' necessario senza calcolarlo nuovamente ogni volta.

La memoization e' proprio il processo di riutilizzo del valore computato precedentemente da una funzione per le successive chiamate che presentano gli stessi valori di input, al fine di rendere il programma piu' veloce.


Infatti i programmatori PHP utilizzano questa tecnica spesso, ma devono fare tutto a mano. In un ipotetico esempio di funzione che dato l'ID dell'utente ritorna il suo username, si potrebbe scrivere qualcosa di simile:
function usernameById($userid) {
    global $_UsernameCache;
    if (!isset($_UsernameCache[$userid])) {
        mysqlQuery("SELECT username FROM user WHERE id= ... ");
        ... altro noioso codice per fare la query ...
        $_UsernameCache[$userid] = $row['username'];
    }
    return $_UsernameCache[$userid];
}
Bleagh!, ogni volta che serve rendere una funzione memoized devo scrivere un tale codice. Eppure e' importante avere la possibilita' di riutilizzare il valore senza fare una query ogni volta, e senza preoccuparmi mentre sto programmando di controllare se avevo gia' tale valore o se lo posso passare alle varie funzioni. Se posso astrarre il fatto che ho gia' computato tale valore tramite una funzione che ci pensa lei a ricordare le precedenti chiamate, perche' no? Il programma diventera' molto piu' leggibile. Sara' da leggere semplice come se fosse cosi' stupido da chiedere ogni volta quel valore, ma al momento dell'esecuzione sara' veloce come uno meno leggibile che passa sempre quel valore precomputato per riutilizzarlo.

In altri linguaggi ci sono modi ben piu' potenti di fare la stessa cosa, ma magari... vediamo se anche con il PHP e' possibile migliorare un po' la cosa. Dopo tutto anche il PHP ha la funzione eval()... per quanto assolutamente bacata e al limite della utilizzabilita'.




Memoization in PHP - esperimento numero uno

Se non conoscete la funzione eval() di PHP vi consiglio di dare una veloce occhiata alla sua documentazione, anche se in due parole e' semplicissimo descriverla. Eval accetta un solo argomento, una stringa che deve essere un programma PHP valido (senza i <? ?> pero'). La funzione quando viene chiamata esegue il pezzo di programma che gli viene dato in input, e... se PHP fosse un linguaggio degno di questo nome ritornerebbe il valore di ritorno dell'ultima espressione computata o qualcosa del genere... invece no, troppo facile avere un comportamento sano eh? PHP sceglie invece di ritornare il valore di ritorno solo se c'e' nello script un return() esplicito.

In pratical Eval("5+5"); non ritornera' 10, mentre Eval("return 5+5"); si. Cerchiamo meglio di capire come funziona Eval e perche' e' importante.

Esempio:
eval("echo('ciao')");
avra' come effetto di scrivere 'ciao'. Che senso ha direte voi, non era possibile semplicemente scrivere echo('ciao')? Certo, la differenza pero' e' che con eval il pezzo di programma da eseguire puo' essere creato dal programma stesso. Qualunque linguaggio di programmazione che presenta Eval() e' almeno in parte un linguaggio di programmazione programmabile.

Per essere un buon linguaggio di programmazione programmabile deve avere altre caratteristiche in realta', come i blocks di Ruby, le macro di Lisp, o uplevel di Tcl, piu' una buonissima dose di introspection. PHP come linguaggio programmabile fa pena, ma bene o male Eval() c'e'.. e la possiamo usare a nostro vantaggio.

Inutile tirarla per le lunghe, sputo l'osso:
function memoize($code) {
    global $_MEMOIZE;
    if (!isset($_MEMOIZE[$code]))
        $_MEMOIZE[$code] = eval($code);
    return $_MEMOIZE[$code];
}
Questa funzione di sole quattro righe fa capire subito la potenza di Eval. Il codice e' banale. Dato uno script in PHP in input a memoize, questa tenta di vedere se ha gia' eseguito tale codice controllando se esiste in un array globale un riferimento a quel preciso input. Se esiste... bene, ritorna direttamente il valore gia' computato. Altrimenti chiama Eval contro quel codice e scrive il risultato per le prossime volte in cui il codice sara' chiamato.

Ricordate la funzione di poco fa che accedeva al DB per ottenere lo username partendo dall'ID dell'utente? Implementava una cache simile tramite un array globale, ora pero' la riscrivo in maniera normale, senza la cache:
function usernameById($userid) {
    mysqlQuery("SELECT username FROM user WHERE id= ... ");
    ... altro noioso codice per fare la query ...
    return $row['username'];
}
Forti della nostra funzione memoize, tutto cio' che ci serve per avere una versione memoized di usernameById e' scrivere il seguente codice:
function memoUsernameById($userid) {
    return memoize("return usernameById(".$userid.");");
}
Notare quanto e' brutto il return che dobbiamo prefissare alla chiamata di usernameById nell'argomento di memoize. Se PHP avesse ritornato in ogni caso il valore dell'ultima espressione computata come fanno praticamente tutti i linguaggi eval-dotati non ci sarebbe stato questo problema... pazienza.

Non solo, ci tocca pure scompattare il valore di $userid prima di passarlo in pasto a memoize, e c'e' da stare attenti sul fronte della sicurezza. Se lo userid viene dall'esterno ci potrebbero essere pezzi di codice PHP dentro (invece di un numero), che l'attacker utilizza perche' sa che poi vengono dati in pasto ad Eval.

Un fix per questo problema e' aggiungere la seguente riga prima della chiamata a memoize:
$userid = intval($userid);
E' orrido ma e' una protezione.




Giusto per capirci...

In linguaggi piu' potenti la memoization e' una cosa assolutamente naturale. Ad esempio in Tcl si puo' scrivere una procedura memoize che basta chiamare come primo comando di una qualunque procedura e questa diventa automagicamente memoized! La stessa cosa vale per Ruby, e ci sono metodi simili per Scheme (un linguaggio cardine della programmazione funzionale, e dunque in larga parte anche della memoization) e Python ed un sacco di altri linguaggi seri.




Fibonacci!

Torniamo a PHP... dove ci puo' portare la nostra memoize? Prendiamo il caso di funzioni pesanemente ricorsive come ad esempio la funzione di Fibonacci.

Ecco la sua definizione.
Fib(1) = 1
Fib(2) = 2
Fib(N) = Fib(N-2)+Fib(N-1)
In PHP si esprime in questo semplice modo:
function fib($n) {
    if ($n <= 2) return 1;
    return fib($n-1)+fib($n-2);
}
Se ci pensate un po' per calcolare fib(30) prima bisogna calcolare fib(29) e fib(28), che in turno calcoleranno fib(27) e fib(26), e fib(28) e fib(27), e cosi' via. In breve la funzione verra' richiamata migliaia di volte per gli stessi valori in uno spreco enorme... con il risultato che fib(30) ci mette un bel po' di secondi per essere calcolata sul mio P4.

Se la funzione fib fosse cosi' furba da mettere in una cache i valori computati sarebbe velocissima, infatti e' una funzione pura, duqnue perche' no? Basta riscriverla in questo modo:
    function memofib($n) {
    if ($n <= 2) return 1;
    $a = memoize("return memofib(".($n-1).");");
    $b = memoize("return memofib(".($n-2).");");
    return $a+$b;
}
e il tempo di esecuzione di fib(30) passa da molti secondi a pochissime frazioni di secondo. Non male eh? In pratica la memoization e' anche un modo per evitare di trasformare funzioni che ricorsivamente sono espresse in maniera molto elegante ma costosa in funzioni iterative. A volte basta la memoization per renderle efficienti, altre volte si puo' eliminare la ricorsione di coda, ma questo e' un argomento successivo... magari oggetto di un nuovo post.




Qualche spunto

Immaginate di scrivere una funzione simile a memoize, che invece pero' di salvare il valore di ritorno, tramite le funzioni per la gestione dell'output del PHP (ob_start & company) catturano l'output dello script chiamato. Ora fate una piccola modifica: invece di salvare il valore precedentemente computato in un array globale... salvatelo su un file (che ha come nome ad esempio il base64 dello script o qualcosa del genere).

Aggiungiamo ora anche un controllo: se il file viene trovato, ovvero se quel codice era stato eseguito e il suo output salvato, ma cio' e' accaduto piu' di N secondi fa, il codice viene nuovamente valutato via Eval e la cache sul filesystem aggiornata.

Complimenti: avete ottenuto una funzione che vi permette di fare il caching selettivo di porzioni della vostra applicazione PHP senza alcuno sforzo.

Se ad esempio avete una funzione che genera tag popolari da un database chiamata genPopularTags() vi bastera' scrivere (immaginando di chiamare evalCache la funzione descritta) il seguente codice:
evalCache("genPopularTags()",300);
Dal canto suo getPopularTags() e' una normalissima funzione che fa una query al DB e genera i tag popolari. Solo che ora, anche se la vostra pagina e' servita un trilione di volte al secondo la funzione verra' chiamata solo una volta ogni 300 secondi (ho immaginato che il secondo argomento fosse il time to live del file di cache), e i vostri DB e CPU sentitamente ringraziano.

Tutto questo senza toccare il codice di generazione, l'unica accortezza che dovete avere e' quella di scrivere funzioni separate per la generazione di parti separate, che e' un'ottima idea a prescindere dal caching.

Considerato che sono le 2:48 e che avro' fatto migliaia di errori di ortografia... posto questo articolo e me ne vado a letto. Grazie se siete arrivati fin qui.

Vi lascio con una massima, che ho letto ieri.

Il compito di un programmatore non e' essere migliore di un altro o il miglior programmatore al mondo. Il suo compito e' quello di essere migliore di come era l'anno scorso.


Amen

2643 views*
Posted at 13:16:48 | permalink | discuss | print