Nuovi linguaggi, vecchi errori

YAPL: Yet Another Programming Language. Da un po’ di anni a questa parte, con la diffusione di strumenti di sviluppo comodi e semplici per la generazione di interpreti e/o compilatori, si sta assistendo a un’autentica proliferazione di nuovi linguaggi di programmazione che cercano di ritagliarsi uno spazio in appositi ambiti applicativi o addirittura di proporsi come sostituti di altri “storici” e consolidati.

E’ il caso di Rust di Mozilla Foundation, che strizza l’occhio al C con una sintassi che ha molto in comune, sebbene non l’abbia presa in prestito a tutti i costi, proponendo nuovi costrutti e funzionalità che lo arricchiscono per snellire “pattern” di utilizzo.

L’obiettivo dichiarato è quello di poter gestire (meglio) grossi progetti, con un occhio di riguardo alla solidità (la gestione della memoria è molto raffinata, e molti costrutti nascono per evitare errori di programmazione molto comuni che portano a problemi di de/allocazione e buffer overflow), forse memori dell’esperienza accumulata con FireFox.

La semplicità del C è, manco a dirlo, abbondantemente superata e il linguaggio, che all’apparenza non sembra complesso (il paragone col C++ è naturale in questi casi), richiederà tempo per assimilare i tanti strumenti che mette a disposizione.

Da questo punto di vista forse il termine corretto da usare per il C è che in realtà sia “scarno” e non necessariamente “semplice”, anche perché è facile impelagarsi in discussioni filosofiche senza fine sul significato di semplicità e complessità.

Debbo dire in tutta sincerità che l’esperienza mi ha portato a maturare un mio personalissimo pensiero sull’argomento “cosa mi piace di un linguaggio di programmazione”, che è imperniato sul concetto di leggibilità. Oltre che scriverlo, trovo quindi che sia piacevole rileggere il codice, a maggior ragione se, oltre a me che l’ho scritto, dovranno farlo anche altri.

Da questo punto di vista non ho fatto mistero di non apprezzare il C, per la maniacale ricerca della stringatezza nella sua sintassi, che porta a scrivere meno codice (meno caratteri, per la precisione) con lo scopo dichiarato di velocizzare la digitazione (ci si doveva sbrigare a scrivere Unix).

Ricordo, per fare un esempio, che la scelta per l’operazione di assegnazione è stata fatta su base meramente statistica poiché è la più comune, per cui hanno preferito il singolo carattere = al più diffuso (all’epoca, dominata da linguaggi Algol-style & derivati) := (che si ritrova spesso nello pseudocodice).

Rust purtroppo ha seguito la stessa strada, e lo si legge già dalle prime battute nel tutorial introduttivo, dove subito dopo la presentazione di un pezzo di codice che riporta una funzione:

fn fac(n: int) -> int {
 let result = 1, i = 1;
 while i <= n {
 result *= i;
 i += 1;
 }
 ret result;
}

si legge a commento:

Also, there’s a tendency towards aggressive abbreviation in the keywords—fn for function, ret for return.

L’elenco delle keyword non mostra, quindi, sorprese:

alt assert
be break
check claim class const cont copy
do
else enum export
fail false fn for
if iface impl import
let log
mod mutable
native
pure
resource ret
true type
unsafe use
while

Si nota subito un uso smodato delle contrazioni, che raggiunge il suo apice coi due soli caratteri dedicati alla keyword che apre la definizione di una funzione: fn.

Purtroppo quel che si nota in queste scelte è una mancanza di stile, di “filosofia” o potremmo chiamarla anche “identità” del linguaggio, che traspare anche in altre scelte.

Infatti le contrazioni non seguono una logica precisa. In molti casi la parola viene troncata dopo un certo numero di caratteri (alt dovrebbe essere l’abbreviazione di alternative, che sostituisce il costrutto switch di C & co.), mentre in altri viene mantenuta la prima lettera, eliminati alcuni caratteri e lasciata la parte finale. fn è il caso più anomalo, dove alla prima lettera se n’è aggiunta un altra che in qualche modo dovrebbe richiamare alla mente function.

Che l’approccio seguito sia statistico, tenendo fede al numero di caratteri digitati, viene confermato subito dopo quando si parla delle parentesi graffe, divenute obbligatorie per racchiudere i blocchi di codice. L’autore, infatti, si affretta a precisare:

If the verbosity of that bothers you, consider the fact that this allows you to omit the parentheses around the condition in if, while, and similar constructs. This will save you two characters every time.

Certe scelte, però, appaiono strane, e la stessa definizione di funzione sopra riportata ne offre un paio di esempi. Il primo riguarda la specificazione del tipo di dato restituito dalla funzione; troviamo, infatti, il simbolo -> (freccia a destra) usato allo scopo, quando si sarebbe potuto utilizzare nuovamente il : e, al contempo, mantenere una certa “filosofia” del linguaggio.

Decisamente più anomala appare la definizione delle variabili locali, che addirittura introduce e fa uso della keyword let quale “prefisso” del costrutto sintattico che porta non soltanto alla definizione di una o più variabili locali, ma anche alla loro eventuale inizializzazione.

Altri linguaggi, come il Go di cui abbiamo già parlato , hanno preferito far ricorso al già citato simbolo := in contesti simili, ottenendo anche l’agognata riduzione dei caratteri digitati.

Purtroppo l’errore più grave di questa scelta è rappresentato dall’aver trascurato un fatto molto importante: all’interno di una funzione la stragrande maggioranza delle assegnazioni nonché dichiarazioni riguarda le variabili locali e non quelle globali (o di modulo / unit, per chi ha questo concetto), per cui sarebbe stato saggio e auspicabile riservare la normale assegnazione a esse, introducendo apposite keyword (glob? gbal? global sarebbe troppo convenzionale e… lungo) diversamente.

Preciso che lo scopo per cui è stato introdotto il costrutto let riguarda in particolare l’uso della type inference per determinare automaticamente il tipo dell’espressione assegnata alla variabile, in modo da evitarne, ancora una volta, la digitazione. Una cosa di cui, comunque, si sarebbe potuto far carico il compilatore. let viene più raramente usato anche per spacchettare tuple (sequenze) di valori in un elenco di variabili.

Sempre all’insegna del risparmio della digitazione dei caratteri è la scelta di definire costrutti sintattici quali l’if, come pure la stessa dichiarazione della funzione, come espressioni, i cui blocchi di codice restituiscono l’ultimo valore calcolato. In questo modo è facile per una funzione evitare persino quei 3 caratteri della già ristretta keyword ret (più lo spazio a seguire, generalmente, e il ; finale: quindi almeno altri 5 caratteri risparmiati!).

Continuando a scorrere il tutorial la situazione non cambia: è un tripudio di costrutti sintattici che mirano alla sistematica riduzione dei tempi di digitazione. Il già citato costrutto alt, che sostituisce il famigerato switch, è uno degli esempi più eloquenti:

alt my_number {
 0 { std::io::println("zero"); }
 1 | 2 { std::io::println("one or two"); }
 3 to 10 { std::io::println("three to ten"); }
 _ { std::io::println("something else"); }
}

la keyword è, di per sé, più corta, ma sono anche completamente sparite le altre keyword case e la label default. Comunque alt non sostituisce soltanto lo switch, ma è un potente strumento basato sul pattern matching che i programmatori Prolog o Erlang apprezzeranno.

In un linguaggio moderno (quindi non si capisce perché Java non le abbia ancora) non potevano non essere presenti le famigerate closure, ma Rust ne mette a disposizione diverse “varianti”. Non essendo una recensione del linguaggio mi limito a riportarne alcuni esempi che mirano a evidenziare il consolidato mantra della velocità di digitazione:

fn call_closure_with_ten(b: fn(int)) { b(10); }

let x = 20;
call_closure_with_ten({|arg|
 #info("x=%d, arg=%d", x, arg);
});
use std;

fn mk_appender(suffix: str) -> fn@(str) -> str {
 let f = fn@(s: str) -> str { s + suffix };
 ret f;
}

fn main() {
 let shout = mk_appender("!");
 std::io::println(shout("hey ho, let's go"));
}

L’aggiunta del solo carattere @ ad fn differenza i due tipi, ma usando la tilde (quindi fn~ anziché fn@) se ne aggiunge un altro ancora…

Lo stesso meccanismo è stato usato per i puntatori. A quelli classici del C, definiti al solito col costrutto *Tipo, se ne aggiungono altri due tipi che fanno uso di @ e ~ al posto dell’* (mentre per la derefenziazione si continua a usare l’operatore *).

Anche le vecchie struct non sfuggono all’opera riduzionista, e si presentano senza apposita keyword, appoggiandosi alla più generale type che definisce un nuovo tipo:

type person = {name: str, address: str};

Per il passaggio di parametri a una funzione, di cui quest’ultima diventa “proprietaria” (facendosi, quindi, carico dell’eventuale deallocazione), fa uso del simbolo + (ma forse sarebbe stato meglio ~, per coerenza con le altre scelte già fatte) prefisso al nome del parametro:

type person = {name: str, address: str};
fn make_person(+name: str, +address: str) -> person {
 ret {name: name, address: address};
}

Per chiudere, sono presenti anche i generic con la classica sintassi C++/C#/Java/etc. che ben conosciamo, la quale contribuisce ad aumentare ulteriormente l’uso di caratteri non alfabetici all’interno del codice che, a questo punto, è facile immagine disseminato di simboli, e la cui leggibilità risulta tutt’altro che scontata.

Mi chiedo che senso abbia, nel 2012, continuare a effettuare scelte basate sulla ricerca della stringatezza a tutti i costi, quando un programmatore con un po’ di esperienza non soltanto riesce a digitare velocemente sulla tastiera, ma lo fa anche meglio se si limita alle sole lettere, senza ricercare tasti speciali o, peggio ancora, loro combinazioni per far apparire il simboletto desiderato.

Soprattutto in piena era degli IDE, dove il completamento automatico del codice è ormai disponibile in tutte le salse e per tutti i linguaggi, è veramente difficile cercare di giustificare questa sistematica castrazione delle keyword nonché abuso del simbolismo per esprimere concetti.

Non si chiede un ritorno a linguaggi come il COBOL, dove il prerequisito all’utilizzo è rappresentato da una laurea in lettere, ma un giusto compromesso fra le parole, alle quali da esseri umani siamo molto ben abituati, e qualche simbolo che è ormai entrato nel nostro comune utilizzo (uguale, minore, maggiore, punto, virgola, ecc.).

In aggiunta, e per chiudere, da un linguaggio che nasce per essere più solido e per grandi progetti non ci si aspetterebbe l’uso di funzioni come printf del C, i cui parametri variabili possono non coincidere col tipo dichiarato nella stringa di formattazione, ma di appositi costrutti.

Eppure Rust le pesca a piene mani proprio dal C, sebbene offra la possibilità di sfruttare delle macro per controllare che il tipo di ogni parametro sia quello riportato nella stringa (per le stringhe “in chiaro”, cioè definite come letterali).

Macro che mi portano alla mente i tempi degli assemblatori; roba di ben difficile portabilità e, soprattutto, manutenibilità.

Alla luce di tutto ciò credo, quindi, che una sana riflessione sia indispensabile per chi si accinge ad accrescere il già ingente numero di linguaggi di programmazione in circolazione, proponendo qualcosa di più allineato alle moderne esigenze di utilizzo di uno strumento importante come questo, col quale si dovrà passare parecchio tempo…

Press ESC to close