Oggi abbiamo a disposizione un gran numero di linguaggi di programmazione, alcuni con un ambito di operabilità molto specifico come il web, l’analisi ed il calcolo matematico, o la gestione dei dati. Questi linguaggi così ad alto livello e facilmente interpretabili dai professionisti dello specifico settore, sono il frutto di anni di evoluzione, che hanno fatto in modo di trasformare un linguaggio binario, interpretabile solo da macchine (codice macchina), passando attraverso livelli di linguaggi di programmazione via via più complessi. In questo articolo vedremo le ragioni e le modalità di questo sviluppo.
I calcolatori digitali
Il calcolatore digitale, noto anche come calcolatore o elaboratore elettronico, è un dispositivo basato su componenti elettronici in grado di effettuare calcoli logici-matematici in risposta a delle istruzioni che gli vengono fornite. Oggi i calcolatori digitali sono più noti come computer, il termine inglese che sta proprio per “calcolatore”.
I calcolatori digitali sono quindi composti da innumerevoli e microscopici circuiti elettronici che vengono progettati e realizzati per riconoscere ed eseguire un insieme limitato di istruzioni semplici, come la somma o lo spostamento tra bit.
Negli ultimi 50 anni di sviluppo informatico, queste istruzioni semplici sono state utilizzate come mattoncini per sviluppare istruzioni via via sempre più complesse, creando una serie di linguaggi di programmazione di base. Queste nuove istruzioni, sebbene più complesse, a loro volta sono state usate come base per la realizzazione di nuove istruzioni e comandi, generando così linguaggi di programmazione via via più elevati, ma allo stesso tempo anche più umanamente leggibili.
I linguaggi di programmazione ci permettono di scrivere dei programmi, che non sono altro che una sequenza di istruzioni o comandi. L’insieme di queste istruzioni definisce il linguaggio di programmazione. E queste istruzioni dovranno in qualche modo essere convertite, o tradotte, in istruzioni più semplici, appartenenti ad un livello inferiore. I linguaggi di programmazione quindi, si sono gradualmente sviluppati uno sull’altro, creando uno schema a strati, chiamati livelli.
Le istruzioni di base: il linguaggio macchina
Quando un calcolatore viene progettato, si tende il più possibile a semplificare le istruzioni base su cui si baserà il suo funzionamento, l’insieme di queste istruzioni definisce il linguaggio macchina, che è il livello più basso di linguaggio di programmazione, dato che è quello che ha direttamente controllo sul funzionamento dei circuiti elettronici all’interno.
Il linguaggio macchina si basa quindi su un set limitato di istruzioni, le più semplici e dirette possibili. Questo ci permette di ridurre la complessità ed il costo dei circuiti elettronici da realizzare.
Inoltre bisogna aggiungere che il linguaggio macchina è specifico di ogni singolo calcolatore. Infatti le istruzioni sono state create specificamente a immagine dell’elettronica all’interno del calcolatore ed essere così altamente efficienti proprio perchè macchina-specifici.
Ogni calcolatore è quindi dotato di un proprio linguaggio macchina, che può differire dagli altri e dipende esclusivamente dalla sua struttura interna. Quindi si può affermare che ogni macchina definisce un linguaggio. Ma è vero anche l’inverso, cioè che ogni linguaggio definisce una macchina.
Questi linguaggi macchina, costituiti quindi da sequenze di numeri e codici alfanumerici, sono facilmente leggibili ed interpretabili dalle macchine, ma non lo sono affatto per gli esseri umani. Nasce quindi l’esigenza di creare un nuovo set di istruzioni che quanto mai si avvicini a concetti familiari agli esseri umani. Si crea così un nuovo livello di programmazione.
Livelli dei linguaggi di programmazione
Chiamiamo L1 il livello del codice macchina, e L2 questo nuovo livello di programmazione. Abbiamo visto che il codice macchina è composto da una sequenza di limitate istruzioni che corrispondono a delle semplici operazioni effettuabili dai circuiti elettronici all’interno del calcolatore.
Quindi sviluppando un programma a livello L2, sarà poi necessario convertirlo nel linguaggio macchina a livello L1, dato che è il solo linguaggio eseguibile da un calcolatore. Le modalità di fare questo possono essere due.
- Metodo di traduzione. Si sostituisce ogni istruzione del L2 nell’equivalente sequenza di istruzioni del L1.
- Metodo di interpretazione. Si scrive un programma in linguaggio L1, chiamato interprete, che sia in grado di ricevere in input i programmi scritti in L2 e li esegua esaminando una istruzione dopo l’altra ed eseguendo per ognuna la sequenza equivalente di istruzioni in L1.
Questo meccanismo tra L1 e L2 si può generalizzare a qualsiasi livello con quello sottostante. Per esempio L4 verrà tradotto in L3 e così via, fino ad arrivare al codice macchina che poi è l’unico veramente eseguibile dal calcolatore.
Nell’analizzare i codici di programmazione da un livello più basso ad uno più alto, vedremo però alcune differenze. Il linguaggio di programmazione utilizzato diventa più complesso, strutturato e con parti testuali maggiori, sempre più simili al linguaggio umano. Diventano infatti via via sempre più leggibili e gestibili da un utente umano. Più il livello è alto, più la sua logica si discosta da quella base del linguaggio macchina.
Il concetto di Macchina Virtuale
Una piccola parentesi nello sviluppo di questo argomento. Abbiamo detto che tutti i livelli di programmazione devono via via essere tradotti scendendo di livello fino ad arrivare al linguaggio macchina, che poi è l’unico interpretabile dal calcolatore, cioè dalla macchina reale.
Ma con il passare del tempo, lo sviluppo di moltissime architetture di calcolatori, e di altrettanti livelli di programmazione ha reso questo schema nella pratica molto complesso. Il programmatore di un determinato linguaggio, spesso non conosce nè i meccanismi, nè il funzionamento dei livelli sottostanti. Sono nate infatti molte realtà professionali, altamente specifiche nell’ambito della programmazione, che ognuna di esse si sviluppa generalmente solo su un livello di programmazione.
Quindi, ritornando allo schema generale, possiamo pensare a ciascun livello di programmazione come specifico e operante per una determinata macchina virtuale. Un caso pratico lo abbiamo per la programmazione Java e la corrispondente Virtual Machine. Il programmatore Java sa che il suo codice si adatterà alla Virtual Machine Java che funziona su tutte le architetture possibili in cui è installata, senza doversi mai preoccupare (o quasi) a livello di codice su che architettura stia lavorando.
La macchina virtuale dovrà essere strutturata in modo da garantire l’esecuzione del programma, indipendentemente dal tipo di architettura, numero di livelli, e linguaggi di programmazione sottostanti.
Con l’introduzione di questo concetto è facilmente intuibile che più è alto il linguaggio di programmazione, maggiore sarà il numero di tipologie di architettture possibili da utilizzare con quel linguaggio. Quindi aumenterà anche la portabilità dei programmi sviluppati con quel linguaggio. Questo in maniera completamente trasparente al programmatore di quel linguaggio.
Schema dei livelli di programmazione nei calcolatori odierni
Mentre i linguaggi macchina, come anche l’assembly, consistono in una serie di istruzioni che includono operazioni su bit di memoria, elementari e focalizzate al dettaglio operativo, il passaggio ai livelli superiori di linguaggio, cioè i linguaggi di sistema, porta ad intravedere nei loro costrutti delle strutture logiche (cose utili agli utenti umani) su cui si può lavorare.
I linguaggi di sistema si focalizzano sulla rappresentazione della memoria e nel rendere gli algoritmi il più efficienti possibile. Vengono quindi introdotte delle strutture logiche e di dati su cui è possibile creare algoritmi e costrutti di memoria su cui lavorare. Si ha quindi a questo livello il primo e vero passaggio per rendere un linguaggio di programmazione umanamente leggibile.
Passando ad un livello ancora superiore, si hanno i linguaggi applicativi. E’ in questi livelli che i costrutti di memoria diventano dei veri e propri modelli di dati, nasce anche la programmazione ad oggetti, con tutta una serie di metodi ed attributi da assegnare a questi costrutti. Si ha un grande guadagno di velocità di programmazione, con una naturale facilità a convertire i problemi del mondo reale in codice scritto. Il tutto però a discapito di una perdita di flessibilità e di visibilità su come viene gestita la memoria a basso livello. E’ importante quindi a questo livello che la traduzione di queste istruzioni nei livelli sottostanti venga effettuata nel modo più efficiente possibile.
Alla fine, ai livelli più alti, troviamo i linguaggi 5GL (5th generation language) e i linguaggi DSL (domani specific language). Questi linguaggi hanno raggiunto completamente il livello di logica umana, e hanno raggiunto un livello di leggibilità tale da essere a volte leggibili anche a chi non è molto esperto nella programmazione, ma esperto nell’argomento specifico in cui lo specifico linguaggio è stato programmato. Infatti i linguaggi a questo livello sono creati per determinati scopi e sono spesso limitati a quello. Esistono linguaggi nati per gestire i database come le diverse versioni di SQL, oppure per creare degli script scientifici (MATLAB), o degli script per applicazioni in altri ambiti professionali (iCAD) e coprono solo questi determinati ambiti. A questo livello di linguaggio così alto, gli algoritmi sottostanti, la gestione delle strutture di memoria, e l’efficienza di un codice non sono più visibili, ma vengono lasciati gestire dai livelli inferiori.