Anche se non ha senso dire che, in generale, un linguaggio è migliore di un altro, né
tanto meno redigere una classifica dei linguaggi di programmazione, è possibile però
individuare ed analizzare le caratteristiche che entrano in gioco nella scelta del
linguaggio da utilizzare in una determinata situazione. Ed è ciò che tenteremo di fare
nel seguito.
Prima di tutto possiamo suddividere tali caratteristiche in due categorie: i fattori intrinseci
ed i fattori ambientali. I fattori intrinseci sono strettamente legati al
linguaggio, alla sua struttura, alla sua filosofia, e possono variare solo se il
linguaggio stesso cambia (con nuove versioni). Quelli ambientali invece fanno riferimento
a come il linguaggio è entrato nel mondo reale, per cui sono legati anche ad altre
variabili non direttamente dipendenti dal linguaggio (potenza degli elaboratori
disponibili, presenza di librerie, diffusione raggiunta, ecc.) e possono quindi anche
variare con il passare del tempo anche se il linguaggio non viene modificato.
E' da sottolineare che molte di queste caratteristiche, per la loro stessa natura, sono
difficili da definire, non sono precisamente quantificabili, spesso sono interdipendenti e
la loro valutazione ha una forte componente soggettiva.
Fattori intrinseci
Nella categoria dei fattori intrinseci abbiamo individuato i seguenti fattori:
espressività, facilità di apprendimento, qualità pedagogiche, leggibilità, robustezza,
facilità di manutenzione, modularità, parametricità, plasmabilità, generalità, grado
di astrazione, efficienza, tipo di traduttore disponibile, complessità del traduttore e,
infine, gestione Input/Output.
Espressività: potremo definirla come la facilità con cui,
utilizzando un determinato linguaggio, è possibile esprimere la soluzione ad un dato
problema. Tale fattore, quindi, determina la velocità con cui possono essere risolti i
problemi mediante l'uso di un linguaggio, la quantità di codice necessaria e persino la
qualità del risultato finale. Infatti il linguaggio utilizzato ha una notevole influenza
anche sul modo in cui si risolve un problema. E' senz'altro applicabile, anche ai
linguaggi di programmazione, quanto ha osservato il linguista Benjamin Lee Whorf sui
linguaggi naturali: "Il linguaggio dà forma al modo di pensare e determina ciò di
cui si può pensare".
La valutazione dell'espressività di un linguaggio dipende, ovviamente, dal tipo di
problema in esame e da quanto il linguaggio sia congeniale al suo utilizzatore tipo.
Ad esempio scrivere in Lisp un programma che calcoli la derivata simbolica di una
espressione matematica, richiede solo 20/30 righe di codice. Usare il C comporterebbe una
quantità di codice notevolmente maggiore, con un maggiore sforzo da parte del
programmatore, ottenendo comunque un risultato, probabilmente più efficiente, ma di
qualità molto minore (in termini soprattutto di leggibilità e modificabilità del
codice). Ciò perché il Lisp, per sua natura, è un linguaggio particolarmente orientato
alla manipolazione simbolica.
Facilità di apprendimento: nella valutazione di tale fattore
vanno principalmente considerati la complessità del linguaggio, il tipo di background
culturale necessario al suo apprendimento e anche un fattore esterno: la presenza di buoni
manuali.
Il Basic (Beginners All-purpose Symbolic Instruction Code) deve la sua diffusione proprio
alla sua estrema semplicità, in quanto ha poche regole grammaticali e può essere
imparato in poco tempo (notare che qui e di seguito, se non altrimenti specificato quando
parliamo di Basic, intendiamo fare riferimento al linguaggio originale, non alle sue
evoluzioni quali il Visual Basic).
Dall'altra parte il C, malgrado il suo numero ridotto di istruzioni e di regole, risulta
essere di non facile apprendimento a causa della sua notazione, a volte oscura, e
soprattutto per le sue caratteristiche a basso livello e, in particolare per l'ampia
possibilità di utilizzo dei puntatori. In realtà i progettisti del C, nella definizione
del linguaggio, hanno sempre puntato verso l'efficienza dell'eseguibile e la semplicità
del compilatore, sacrificando a volte la chiarezza del linguaggio.
Infine, come ultimo esempio, consideriamo il Prolog il cui apprendimento presenta delle
difficoltà sia perché è necessario prima acquisire alcune conoscenze di logica sia
perché richiede, per le sue caratteristiche di linguaggio dichiarativo, un modo di
affrontare i problemi molto diverso da quello usuale.
Qualità pedagogiche: indica l'attitudine del linguaggio ad essere
utilizzato come strumento per l'insegnamento della programmazione.
Il Pascal, nato proprio come veicolo didattico, è in generale considerato il più adatto
a tale scopo, molto più del Basic che, malgrado la sua semplicità, presenta troppe
lacune (ad esempio non è strutturato e manca di strutture dati astratte) che ne
diminuiscono le valenze formative. Oggi un buon candidato può essere considerato anche il
Visual Basic che ha tratto molto proprio dal Pascal, integrandolo alla facilità del
Basic. Un altro linguaggio con una forte vocazione didattica è il Logo, usato soprattutto
per insegnare nozioni di programmazione ai bambini. Tale sua peculiarità ha favorito la
creazione di versioni nazionalizzate, per cui le parole chiavi del linguaggio sono tratte
dall'italiano (e non dal solito inglese). La nazionalizzazione di un linguaggio ha,
ovviamente, senso solo da un punto di vista didattico, in quanto comporta anche svantaggi
non trascurabili, primo fra tutti una minore portabilità del codice e una minore
comunicabilità tra programmatori di differenti lingue.
Notiamo esplicitamente che un linguaggio può avere caratteristiche formative pur non
essendo di facile apprendimento o non adatto a neofiti. Come caso limite, possiamo
considerare lo stesso Assembler il cui insegnamento risulta molto formativo, in quanto
mostra le operazioni base eseguibili da un microprocessore.
Interessanti, da questo punto di vista, sono da considerarsi anche il Lisp e il Prolog sia
per la loro originalità sia perché imparandoli si acquisiscono tecniche di risoluzione
di problemi efficacemente utilizzabili, in determinate situazioni, anche negli altri
linguaggi di programmazione più "normali".
Leggibilità: è in relazione con la facilità con cui il
progettista e/o persone estranee ad un progetto possono leggere e comprendere il relativo
programma. Ovviamente questa qualità, riferita ad un determinato programma, dipende
principalmente dallo stile di programmazione usato e dal rispetto delle regole di buona
educazione (uso di nomi significativi, utilizzo appropriato delle indentazione, rispetto
delle convenzioni universalmente accettate, ecc.); ciononostante essa può essere
facilitata o meno dal linguaggio di programmazione. Ad esempio solo i linguaggi più
moderni concedono piena libertà nel posizionare le istruzioni, permettendo così
l'indentazione.
Uno dei linguaggi meno leggibili è senz'altro l'APL (A Programming Language). Nato per
risolvere problemi scientifici e matematici, con forte presenza di dati in vettori e
matrici, fa largo uso (oltre al normale insieme di caratteri ascii) di simboli speciali,
che lo rendono, ai non iniziati, del tutto illeggibile.
E' necessario, comunque, distinguere tra leggibilità per neofiti e per esperti. Ad
esempio un, a prima vista illeggibile ed ostico,
i++;
utilizzato in C per incrementare una variabile, per un programmatore esperto è molto più
leggibile di un ridondante
i:=i+1;
Ancora, se è vero che la prolissità del Cobol lo rende leggibile anche da parte di non
programmatori, per un matematico (ma non solo) dover usare espressioni del tipo:
MULTIPLY BASE BY ALTEZZA GIVING AREA
è non solo poco naturale, ma nel caso di espressioni più complesse, praticamente
illeggibile. Molto meglio il più conciso e soprattutto simile alla notazione algebrica:
AREA=BASE*ALTEZZA
che, per fortuna, si utilizza nella maggior parte degli altri linguaggi.
La leggibilità di un linguaggio non è assoluta ma dipende anche dal tipo di problema
affrontato e dalla lunghezza del codice utilizzato per la sua soluzione. Ad esempio i
programmi scritti in Basic sono di immediata comprensione fin tanto che restano di
dimensioni limitate. Quando comincia a crescere la complessità del programma, la mancanza
di costrutti strutturati rende sempre più difficoltosa la lettura del sorgente.
Ancora, la risoluzione di alcune tipologie di problemi con il Prolog, sono costituiti da
programmi di una leggibilità esemplare. In molti altri casi è necessario rincorrere a
contorsioni algoritmiche che portano, invece, ad un codice completamente ermetico.
Robustezza: è la capacità del linguaggio (a volte a costo di una
minore flessibilità e potenza) di impedire al programmatore, per quanto possibile, di
inserire dei bug all'interno del codice. Ovviamente anche qui, come nel caso della
leggibilità, molto dipende dal programmatore, dal suo stile, dall'uso di tecniche di
programmazione difensiva.
Il linguaggio meno robusto è senz'altro l'Assembler, in quanto dà libertà illimitata al
programmatore, permettendogli addirittura di creare codice automodificante. I linguaggi ad
alto livello, invece, hanno via via introdotto una serie di caratteristiche atte proprio a
diminuire il rischio di incorrere nei bug. Ad esempio la programmazione strutturata,
limitando i tipi di controllo di flusso (in pratica vietando i GOTO indiscriminati) e
favorendo una modularizzazione del codice va proprio in questa direzione. Così anche il strong
type checking (controllo rigoroso dei tipi), oltre a permettere la creazione di
eseguibili più efficienti, costituisce un modo efficace per evitare insidiosi
bug.
Fra i linguaggi robusti si distingue il Pascal che è pervaso da un forte "senso
materno", al contrario del C che segue, invece, la filosofia secondo cui se un
programmatore fa una determinata azione, anche se potenzialmente pericolosa, si deve
presupporre che sa cosa sta facendo, per cui bisogna lasciargliela fare.
Facilità di manutenzione: intesa come facilità con cui è
possibile modificare un programma per venire incontro a nuove esigenze o per eliminare
problemi emersi nel suo uso. Il fatto è che tutti i programmi, che riescono a suscitare
un minimo di interesse, sono soggetti ad una continua evoluzione. Si stima che gran parte
del tempo di sviluppo è impiegato non nella creazione di nuovi programmi ma nella
modifica di programmi esistenti. Da qui l'importanza notevole di tale fattore, che risulta
essere fortemente legato alla modularità del linguaggio. Questo fattore è anche
il motivo del considerevole successo avuto dai linguaggi ad oggetti, che pur rallentando
le normali fasi di sviluppo (richiedendo un maggior sforzo di analisi iniziale per
individuare e organizzare le gerarchie di classi opportune) facilitano drasticamente la
manutenzione nel tempo del software.
Modularità: è la capacità del linguaggio di favorire
suddivisione di un programma in parti di codice quanto più indipendenti l'uno d'altro.
Ciò facilità la creazione e la manutenzione del programma (principio del divide et
impera) ed, inoltre, aumenta la possibilità di riutilizzare lo stesso codice per
altre applicazioni. Solitamente ciò si raggiunge con la possibilità di avere
sottoprogrammi (procedure, funzioni, subroutine, ecc. a secondo della terminologia del
linguaggio) e con tecniche di information hiding per avere delle scatole nere di
cui all'esterno si può accedere solo attraverso un ben determinato protocollo.
Parametricità: cioè la capacità di poter creare programmi che
consentono la risoluzione di più problemi contemporaneamente.
Il Prolog, ad esempio, in alcuni casi (purtroppo solo in problemi relativamente semplici)
risulta essere estremamente potente, proprio grazie alle sue caratteristiche di linguaggio
dichiarativo. Esemplare è il codice che permette di calcolare la lista unione di due
liste. Esaminiamone le due regole che definiscono il predicato append
(in corsivo ne riportiamo il relativo significato per maggiore chiarezza):
append([], L, L).
se la prima lista è vuota allora l'unione (la terza lista) è uguale alla seconda
append([T|C], L, [T|R]):- append(C, L, R).
la lista unione è uguale al primo elemento della prima lista seguita dalla lista che
è l'unione della restante parte della prima lista e della seconda
Questo codice è molto simile a ciò che si potrebbe scrivere in Lisp dato che
entrambi supportano le liste come tipo di dato built-in (ovvero predefinito), e
supportano facilmente le definizioni ricorsive. Infatti in Lisp possiamo definire la funzione
append in questo modo:
(defun append (l m)
(if (null l)
m
(cons (car l) (append (cdr l) m))))
Però vi è una importante differenza: mentre il codice Lisp permette solo di calcolare
l'unione di due liste, il codice Prolog riportato permette anche di effettuare altre
operazioni correlate: data una lista scomporla in tutti i modi possibili in due segmenti
consecutivi separati di cui essa è l'unione, oppure data una lista e una sua parte
iniziale di ottenerne la parte rimanente, e così via. Ciò in quanto il codice Lisp
specifica come calcolare appunto l'unione di due liste (approccio procedurale),
mentre il codice Prolog specifica solo cosa si intende dire quando si dice che
una lista è l'append di due liste (approccio dichiarativo).
Plasmabilità: la possibilità di poter modificare o estendere la
sintassi del linguaggio. I linguaggi tradizionali (Pascal, Cobol, C, ecc.) presentano poca
flessibilità da questo punto di vista. Invece il Prolog permette di definire nuovi
operatori (con priorità e tipo di associazione desiderato), e anche il Lisp data la
intrinseca uguaglianza tra programmi e dati (le funzioni non sono altro che liste)
presenta un buon grado di plasmabilità. I linguaggi ad oggetti presentano in genere un
notevole grado di plasmabilità. Ad esempio il C++ permette l'overloading degli operatori,
per cui si possono ridefinire gli usuali operatori (+, -, *, ecc.) a secondo del tipo di
dati su cui operano. Per cui, ad esempio, il C++ non supporta direttamente come strutture
dati le liste, ma data la sua versatilità non solo si possono definire come tipo utente
ma, ridefinendo opportunamente i vari operatori necessari, renderle praticamente
indistinguibile dai tipi base.
Generalità: il grado con cui il linguaggio può essere utilizzato
in diversi campi di applicazione. Ci sono molti linguaggi che anche se non hanno raggiunto
una larga diffusione hanno però trovato una propria nicchia ad esempio il Forth diffuso
nell'ambito del controllo di processo.
Così come il Cobol che difficilmente viene utilizzato al di fuori dall'area dei
gestionali.
Ben definizione: la sintassi e la semantica del linguaggio devono
essere privi di ambiguità e avere una coerenza interna.
Grado di astrazione: si riferisce al livello di astrazione sui
dati e sul controllo. Alcuni linguaggi permettono più facilmente di effettuare la Metaprogrammazione:
ovvero di creare programmi che controllano lo stato della propria esecuzione, agendo di
conseguenza.
Efficienza: è la capacità del linguaggio di creare applicazioni
efficienti sia in termini di memoria che di velocità. I linguaggi più astratti tendono a
produrre codice più inefficienti sia perché lo sono strutturalmente sia perché, proprio
per la loro astrazione, risulta più semplice scrivere programmi inefficienti con essi.
Ad esempio in Lisp viene naturale calcolare i valori della successione di Fibonacci (in
cui ogni elemento è la somma dei due valori precedenti: 1, 1, 2, 3, 5, 8, 13, ...)
ricalcandone fedelmente la definizione ricorsiva:
fib(1) = 1; fib(2) = 1; fib(n) = fib(n-1) + fib(n-2)
ottenendo il seguente codice:
(defun fib(n)
(if (or (= n 1) (= n 2))
1
(+ (fib (- n 1)) (fib (- n 2)))))
che va calcolare più e più volte gli stessi valori in una maniera terribilmente
inefficiente!
Tipo di traduttore disponibile: esistono due tipi di traduttori, interpreti
e compilatori. Il primo partendo dal file sorgente traduce un istruzione alla
volta, la manda in esecuzione, e quindi passa alla istruzione seguente. Un compilatore,
invece, traduce l'intero programma in un colpo solo, ottenendo un file direttamente
eseguibile. La compilazione risulta in una esecuzione molto più veloce, mentre
l'interpretazione permette di sviluppare un programma in maniera più interattiva.
Anche se in linea di principio tutti i linguaggi potrebbero essere tradotti in entrambi
modi spesso è disponibile solo un tipo di traduttore, o comunque il linguaggio non
risulta essere, per le sue caratteristiche, molto adatto ad un tipo di traduzione. Ed è
questo il motivo per cui lo consideriamo un fattore di tipo intrinseco.
Ad esempio il C è un linguaggio tipicamente compilato, mentre il Basic è interpretato
(anche se per entrambi è disponibile l'altro tipo di traduttore).
Complessità del traduttore: la struttura del linguaggio può
essere tale da richiedere, per l'operazione di traduzione, notevoli risorse di memoria e
tempi rilevanti. Inoltre bisogna considerare anche la difficoltà di implementazione del
traduttore. La complessità del traduttore è influenzata non solo dalla struttura di base
del linguaggio ma anche da alcune scelte, marginali ai fini della personalità del
linguaggio, fatte dai progettisti. Ad esempio in PL/I le parole chiavi (ovvero le
parole che fanno parte integranti del linguaggio) non sono riservate (anche
perché sono numerose), per cui anche parole chiavi come IF e THEN si possono utilizzare
come identificatori ed è compito del compilatore distinguere i due tipi di uso a secondo
del contesto (complicando così di molto la fase di analisi lessicale, cioè la
fase di identificazione dei simboli presenti nel sorgente).
La complessità del traduttore ha un notevole impatto sulla diffusione del linguaggio.
Gestione Input/Output: la presenza di strumenti atti a facilitare
l'accesso sequenziale e casuale ai file e il supporto all'utilizzo di database. Alcuni
linguaggi danno un supporto diretto ed esteso all'I/O (primo fra tutti il Cobol), altri
hanno bisogno di librerie esterne, altri ne sono del tutto privi.
Fattori ambientali
I fattori ambientali sono: diffusione, integrazione, ambienti di sviluppo disponibili,
portabilità e grado di standardizzazione
Diffusione: quanto più l'utilizzo di un linguaggio è diffuso,
maggiore è la disponibilità di documentazione, librerie, strumenti di sviluppo,
piattaforme su cui è implementato, e infine c'è un maggiore sforzo da parte dei
produttori (anche per la maggiore concorrenza che si viene a creare) per sviluppare
compilatori efficienti e che producono codice efficiente.
Integrazione: come abbiamo detto a secondo del tipo di
applicazione un linguaggio può essere più o meno adatto; per grossi progetti può essere
quindi utile sviluppare i vari moduli in linguaggi diversi, in modo da usufruire delle
migliori caratteristiche di ciascuno. Per integrazione, quindi, intendiamo la possibilità
di chiamare codice scritto in altri linguaggi e viceversa.
Ambienti di sviluppo disponibili:
malgrado gli sforzi attuali per
sviluppare una metodologia sistematica (ingegneria del software), la
programmazione è ancora in gran parte un'arte, per cui rivestono notevole importanza gli
strumenti di supporto a tale attività: editor integrato, debugger simbolici,
profiler,
help on line, ecc. Tale fattore è in generale influenzato dalla diffusione, con alcune
pregevoli eccezioni (Lisp).
Portabilità: è strettamente legato alle piattaforme
hardware/software su cui è implementato. Spesso è determinante la presenza di una
standardizzazione ufficiale (ma non essenziale ad esempio nel caso del C, il libro di
Kernighan e Ritche ha determinato uno standard de facto).
Grado di standardizzazione: anche i linguaggi di programmazione,
come quelli naturali, soffrono della presenza dei dialetti. Per cui di uno stesso
linguaggio ne possono esistere più versioni tra loro incompatibili anche per piccoli
particolari. Ciò, come per i dialetti naturali, porta a problemi di incomunicabilità:
per poter utilizzare un programma scritto in un altro dialetto, è necessario apportarvi
delle modifiche (a volte lievi, a volte sostanziali).
Il linguaggio che soffre di più di tale situazione è il Basic. Anche il Lisp in
gioventù, ne ha sofferto, poi è stato creato un apposito comitato per la
standardizzazione che ha prodotto il Common Lisp, che ora ha quasi del tutto soppiantato i
precedenti.
Di norma è accettabile un certo grado di tolleranza verso estensioni del linguaggio resi
disponibili dalle varie implementazioni, purché queste non influenzino il nucleo
originale e siano ben distinte da queste (magari disattivabili mediante opzioni del
compilatore). Anzi tale usanza risulta essere determinante per l'evoluzione del linguaggio
stesso, rappresentando un palestra dove sperimentare nuovi costrutti, e spesso quelli che
hanno riscosso un maggior successo vengono inserite nelle versioni standard successive del
linguaggio.
Conclusioni
Quale linguaggio bisogna imparare allora? Per chi inizia è senz'altro da consigliare
il Visual Basic. In ogni caso è importante tenere presente quando si impara un nuovo
linguaggio, che bisogna capirne la filosofia di base, l'idea o le idee che stanno sotto.
Molti programmatori lavorano in C, come se stessero usando il Pascal (addirittura alcuni
utilizzano dei zuccheri sintattici definendo delle macro del tipo begin
e end uguali alle parentesi graffe, facendo scarso uso dei puntatori e così
via). Ancora, programmare in Lisp vuol dire entrare in un modo diverso di vedere le cose:
in Lisp è naturale costruire tante piccole funzioni che si richiamano tra di loro, anche
se il Lisp permette un tipo di programmazione imperativo che risulta però non naturale da
usare.
In generale è preferibile conoscere più di un linguaggio, sia per essere in grado di
poter scegliere il più adatto in base al tipo di applicazione da sviluppare sia perché,
anche se poi ne andiamo ad usare uno solo, avere una conoscenza più ampia ci permette di
comprenderlo meglio e quindi in definitiva di saperlo utilizzare meglio.
In conclusione possiamo dire che c'è nella comunità internazionale uno sforzo continuo
sia nel migliorare linguaggi già esistenti sia di crearne di nuovi. L'obiettivo di fondo
è creare linguaggi che permettano di sviluppare in minor tempo, programmi migliori.
© Massimo Di Bello <Prometheo
Staff>
mdibello@prometheo.it
Articolo#05: [pubblicato il:
28/06/99]
|