linguàggio di programmazióne In informatica, insieme di parole e di regole, definite in modo formale, per consentire la programmazione di un elaboratore affinché esegua compiti predeterminati. Esistono l. di p. di alto livello e di basso livello: i primi permettono al programmatore di lavorare su vere e proprie operazioni logiche, lasciando a un programma, detto compilatore, il compito di tradurle in istruzioni per il processore scritte in linguaggio macchina, ossia in sequenze di codice binario comprensibili dal processore. I secondi sono più vicini al linguaggio macchina e le istruzioni scritte con tali l. di p. non devono essere perciò sottoposte al lavoro compilatore. (➔ anche linguaggio)
Abstract di approfondimento da Programmazione, linguaggi di di Gian Marco Todesco (Enciclopedia della Scienza e della Tecnica)
Nonostante profonde differenze, formali e sostanziali, i linguaggi di programmazione si strutturano attorno a un certo insieme di concetti comuni, concernenti i dati su cui il programma dovrà intervenire e le operazioni da eseguire su tali dati.
Tutti i linguaggi di programmazione (a esclusione del linguaggio macchina) permettono di attribuire nomi ai dati, per esempio mediante le variabili, gli argomenti delle funzioni e delle procedure, le costanti con nome. La modifica del valore di una variabile durante l’esecuzione del programma è un’istruzione fondamentale nella maggior parte dei linguaggi di programmazione (i cosiddetti linguaggi imperativi). La parte di programma in cui un determinato nome è valido si chiama scope del nome. In molti linguaggi è possibile controllare lo scope di ogni nome, in modo che copra tutto il programma, una singola procedura o anche solo un blocco di istruzioni. Utilizzare lo scope più piccolo possibile diminuisce il rischio di errori e rende più chiaro il codice.
I programmi per computer possono manipolare una gran varietà di dati: immagini, suoni, sequenze di caratteri, numeri, e così via, ma tutti questi dati sono rappresentati in memoria come sequenze di 0 e di 1. La sequenza 01000001 può rappresentare, per esempio, il numero intero 65, la lettera A oppure un frammento di un’immagine in bianco e nero che contiene due pixel bianchi e sei neri. L’informazione che permette di sapere quale sia l’interpretazione giusta si chiama tipo del dato. Ogni linguaggio definisce alcuni tipi fondamentali e spesso permette al programmatore di aggiungere tipi nuovi. Fra i tipi fondamentali si trovano in genere vari tipi numerici, le sequenze di caratteri (chiamate stringhe) e il cosiddetto tipo booleano, che può assumere solo i valori vero e falso. Un altro tipo fondamentale particolarmente importante è il riferimento o puntatore: invece di rappresentare un dato, ne indica la posizione all’interno della memoria. Tale tipo di dato rappresenta la base per costruire strutture articolate, come i grafi o gli alberi, formate da entità collegate fra loro in vario modo.
I tipi elementari possono essere aggregati fra loro formando altri tipi. Le forme più comuni di aggregazione prevedono di identificare le singole componenti con un numero o una ennupla di numeri (formando i cosiddetti array, cioè vettori o matrici) oppure con un nome (dando vita ai record, alle strutture, agli array associativi ecc.).
L’associazione fra tipi e dati è un’importante caratterizzazione del linguaggio. Nei linguaggi staticamente tipizzati (come C, C11, C#, Java, Delphi, ML, Haskell), essa non cambia durante l’esecuzione del programma. Il programmatore deve in genere dichiarare esplicitamente la variabile, specificandone il tipo, anche se in alcuni linguaggi (come ML o Haskell), il compilatore o l’interprete sono in grado di inferire il tipo corretto dal contesto. Assegnare alla variabile un valore di tipo diverso o utilizzarla in un contesto inappropriato per il suo tipo (per es., eseguire un’operazione aritmetica su una variabile di tipo non numerico) genera un errore in fase di compilazione. Proprio il fatto che il compilatore sia in grado di effettuare automaticamente queste verifiche viene considerato uno dei principali vantaggi di questo approccio. Nei linguaggi dinamicamente tipizzati (APL, Objective-C, Lisp, Smalltalk, JavaScript, Tcl, Prolog, Python, Ruby ecc.), il legame fra variabile e tipo può cambiare nel corso dell’esecuzione: la variabile, per così dire, contiene sia il dato che il tipo. Un utilizzo scorretto può essere segnalato solo durante l’esecuzione. I sostenitori di questo approccio sostengono che permetta una programmazione più semplice e rapida.
Un’altra differenza si può definire fra i linguaggi fortemente tipizzati (per es., Ada, Java, ML, Oberon) in cui i dati di tipo diverso non si possono mescolare, e i linguaggi debolmente tipizzati (come C, Assembly, C11, Tcl), nei quali possono essere effettuate delle conversioni (cast).
Ogni tipo prevede un insieme di operazioni fondamentali che possono essere effettuate sui dati corrispondenti. Per esempio, due numeri interi possono essere sommati, sottratti, confrontati e così via. Molti linguaggi definiscono una sintassi particolarmente comoda per esprimere queste operazioni. Vengono utilizzati segni (operatori) che possono essere combinati fra loro in modo da formare espressioni simili all’usuale notazione matematica. Se A, B, C sono tre variabili, è in genere possibile scrivere per esempio A*(B1C)/2 se si intende indicare la somma di B e C moltiplicata per A e divisa per due.
Possono esserci notevoli differenze fra un linguaggio e l’altro. Per esempio, può variare la scelta del carattere (o della sequenza di caratteri) usato per esprimere una determinata operazione: xy si scrive x^y in VisualBasic, x**y in Fortran, mentre in C/C11 non è disponibile come operatore e si deve quindi scrivere pow(x,y). Possono esserci differenze nella gestione delle precedenze: per esempio, l’espressione a1b*c può essere considerata equivalente a a1(b*c) oppure a (a1b)*c. In molti linguaggi il compilatore segue la convenzione usuale e dà la precedenza alla moltiplicazione. Alcuni linguaggi seguono più semplicemente l’ordine da sinistra a destra (per es., Occam), oppure da destra a sinistra (per es., APL). In altri casi (come il Forth o PostScript) si utilizza la cosiddetta notazione polacca inversa, in cui si scrivono prima gli argomenti e poi gli operatori: nel caso precedente si ha a b c * 1.
La semantica dell’operazione a1b dipende in genere dal tipo dei dati coinvolti: può indicare l’addizione fra due numeri, la somma di due vettori, la concatenazione di due stringhe e così via (per es., in alcuni linguaggi si può scrivere 315 che vale 8 mentre ‘3’1‘5’ vale ‘35’). L’associazione di più significati a uno stesso simbolo è detta overloading (letteralmente: sovraccarico). Alcuni linguaggi permettono di creare nuovi tipi e associare ai simboli convenzionali le relative operazioni. È possibile per esempio definire entità matematiche non presenti nei tipi base del linguaggio (tensori, quaternioni, numeri complessi ecc.) e fare in modo che si possa esprimere la moltiplicazione fra queste entità utilizzando lo stesso simbolo * che è associato alla normale moltiplicazione fra numeri.
La scelta di utilizzare caratteri speciali per indicare gli operatori pone un problema quando il numero di questi ultimi diventi grande. Un linguaggio come il C11, relativamente ricco di operatori, ne definisce molti utilizzando coppie di caratteri (per es., , , o ! 5). Un approccio diverso è seguito da APL, che sfrutta caratteri speciali che non si trovano sulle tastiere normali. I programmi scritti in APL sono straordinariamente compatti (benché possano apparire oscuri a chi non conosca bene il linguaggio).
Le istruzioni di controllo del flusso regolano l’ordine di esecuzione delle istruzioni di un programma e sono indispensabili per l’implementazione di ogni algoritmo non banale. L’istruzione fondamentale è il cosiddetto trasferimento condizionato del controllo. In dipendenza da una ben definita condizione (per es., se il valore di una certa variabile è maggiore di 0), l’esecuzione continua in un altro punto del codice, altrimenti prosegue normalmente con l’istruzione successiva. Utilizzando questo costrutto è possibile concatenare frammenti di codice formando strutture arbitrariamente complesse. Il paradigma della cosiddetta programmazione strutturata impone vincoli a queste strutture, con il fine di rendere i programmi più facili da comprendere, correggere e modificare. Sono permesse solo tre strutture di controllo: la sequenza, la selezione e l’iterazione, ognuna con un solo punto di ingresso e un solo punto di uscita. Esse possono essere combinate fra loro a seconda delle necessità.
La maggior parte dei linguaggi di programmazione ha una propria versione dei tre costrutti (oltre a consentire in genere altri costrutti meno ortodossi). La sequenza è semplicemente un blocco di codice eseguito dall’inizio alla fine. La selezione è costituita da uno o più blocchi dei quali ne venga eseguito al più uno in dipendenza di una precisa condizione. Esempi di questo costrutto sono le istruzioni if-then, if-then-else, switch, select, presenti in moltissimi linguaggi con minime variazioni sintattiche. L’iterazione, chiamata anche ciclo o loop, prevede un blocco di codice da eseguire un arbitrario numero di volte (al limite, anche zero). Esempi di iterazione sono for, foreach, while, do-while, repeat-until.
Alcuni costrutti che violano i principi della programmazione strutturata sono il goto e il throw-catch. Quest’ultimo permette di implementare la cosiddetta gestione delle eccezioni nei linguaggi che supportano questo costrutto (Ada, C11, Corn, Delphi, Objective-C, Java, Eiffel, Ocaml, Python, Common Lisp, SML, PHP e i linguaggi della piattaforma .NET). Il meccanismo delle eccezioni permette di trattare separatamente la gestione degli eventi straordinari (per es., gli errori) evitando di appesantire il codice principale con una miriade di controlli.
È pratica comune organizzare i programmi in più porzioni ognuna delle quali risolva un ben definito sottoproblema. I relativi costrutti prendono diversi nomi nei vari linguaggi e in dipendenza dalle loro caratteristiche: procedure, subroutine, sottoprogrammi, funzioni, metodi, messaggi. In ogni caso si tratta di blocchi di codice delimitati con precisione, in genere identificati con un nome e che si comportano come un piccolo programma in miniatura: elaborano dati e forniscono un risultato seguendo un determinato algoritmo. Sono definiti in un punto del programma e possono essere richiamati in altri punti. In corrispondenza della chiamata (detta anche invocazione o attivazione), l’esecuzione del programma salta al codice della procedura, lo esegue e poi riprende dall’istruzione successiva. La procedura può restituire un valore e può avere effetti collaterali, come modificare il valore di una variabile o effettuare delle operazioni di I/O (input/output). Molti linguaggi utilizzano il termine funzione quando viene restituito un valore, procedura negli altri casi. A volte (come in C o in Java) le procedure sono considerate un caso speciale di funzioni il cui valore di ritorno sia irrilevante.
Le procedure limitano la ridondanza del codice, ne migliorano la chiarezza e ne facilitano il riutilizzo: una procedura dalle competenze ben definite, definita nell’ambito di un programma, può servire anche in un altro programma. Questa pratica è molto diffusa. Le procedure il cui riutilizzo appare più probabile sono in genere raccolte in librerie (termine derivato da un’errata traduzione del termine inglese library, che letteralmente significa biblioteca). Accanto alle librerie create man mano dal programmatore ci possono essere librerie di sistema, spesso parte integrante del linguaggio, di cui ne estendono le funzionalità.
Una procedura può richiamare altre procedure (può in genere anche richiamare se stessa: procedimento versatile e potente, noto come ricorsione).
Lo scambio di dati fra la procedura chiamata e quella chiamante può avvenire con diverse modalità: mediante variabili condivise (ovvero il cui scope comprenda chiamato e chiamante), oppure, in alcuni linguaggi (come Forth o PostScript) utilizzando lo stack (pila), un’area di memoria in cui i dati possono essere accumulati e rimossi sequenzialmente. Il sistema più diffuso prevede l’uso di parametri, speciali variabili associate alla procedura e dedicate a questo scopo. L’utilizzo dei parametri è molto articolato e può essere assai differente nei vari linguaggi. I parametri possono essere usati per trasferire dati dal chiamante al chiamato o viceversa; una procedura può essere chiamata con un numero variabile di parametri; si possono definire procedure diverse con lo stesso nome, distinte solo per il numero ed eventualmente il tipo dei parametri; nell’invocazione l’associazione fra parametri e loro valore può essere fatta in base alla posizione, ma anche al nome, e così via.
In alcuni linguaggi (tra cui Lisp, Python, JavaScript, Matlab), le funzioni sono trattate come un tipo di dato: possono essere create, assegnate a una variabile o passate come argomento a un’altra procedura. In questi linguaggi è possibile anche creare funzioni anonime, cioè non associate ad alcun nome.
Le procedure sono casi particolari di un costrutto più generale: le coroutine (definite, tra gli altri, in Simula, Modula-2, C#, Lua, e utilizzabili anche nei linguaggi assemblativi). Le coroutine possono, mediante un’apposita istruzione, restituire il controllo alla procedura chiamante e, alla successiva riattivazione, riprendere l’esecuzione dall’istruzione successiva. Sono un costrutto relativamente poco diffuso, ma assai versatile e potente.
La cosiddetta programmazione orientata agli oggetti (OOP, Object-oriented programming) rappresenta uno dei paradigmi di programmazione più noti e utilizzati, e molti linguaggi hanno costrutti specificamente dedicati a questo paradigma. Un oggetto è un’entità che raggruppa un insieme di dati (attributi o membri) e le operazioni che è possibile effettuare sui dati stessi (metodi o messaggi). Il programmatore progetta in genere gli oggetti in modo che rappresentino entità del mondo reale (un oggetto può, per es., rappresentare una nave, con la sua posizione geografica, la sua velocità o il peso del suo carico e le operazioni relative possono essere la modifica della velocità, l’aggiornamento della posizione in base alla velocità, ecc.). Il paradigma della programmazione orientata agli oggetti si articola attorno a tre concetti chiave: l’incapsulamento, il polimorfismo e l’ereditarietà. L’incapsulamento consiste nella separazione fra l’utilizzo di un oggetto e la conoscenza dei dettagli del suo funzionamento. Il polimorfismo permette a oggetti diversi di eseguire in modo differente la stessa operazione: per esempio, oggetti che rappresentino differenti figure geometriche sono in grado di disegnarsi sullo schermo ognuno in maniera diversa. Non è insolito che un’operazione coinvolga più di un oggetto e può essere utile definire un comportamento polimorfo che tenga conto del tipo di tutti gli oggetti coinvolti (per es., il calcolo dell’intersezione di due figure geometriche dipende ovviamente da entrambe le forme coinvolte). Alcuni linguaggi (CLOS, Dylan, Nice, ecc.) prevedono un costrutto specifico (multimetodi) per gestire questi casi.
In molti linguaggi orientati agli oggetti viene definito il concetto di classe che rappresenta una sorta di stampo per creare gli oggetti. L’ereditarietà è la capacità di definire una classe come specializzazione di una classe più generale (nell’esempio delle figure geometriche possiamo pensare ad una classe Figura da cui discendono le classi Cerchio e Quadrato).
La programmazione orientata agli oggetti è efficace quando il programma può essere diviso in elementi con una ben definita e relativamente limitata interazione reciproca e quando gli algoritmi implementati siano intimamente legati alle relative strutture dati. Altri paradigmi di programmazione possono essere più appropriati in altri casi. Per esempio, la programmazione orientata agli aspetti (AOP, Aspect-oriented programming: ne sono esempi AspectJ e AspectC11, tra gli altri) gestisce meglio le funzionalità che interessano contemporaneamente tanti oggetti diversi, come il monitoraggio delle operazioni svolte (logging) mentre la programmazione generica (C11, D, Beta, Eiffel, Clu, Ada, ML, Java 5.0 ecc.) permette di definire degli algoritmi applicabili a differenti strutture dati non correlate fra loro.