Informatica per script kiddies 9 – I linguaggi di programmazione, una panoramica

È il terzo mercoledì del mese, ed eccoci con Informatica per script kiddies!

Oggi proviamo a dire qualche parola di orientamento sulle caratteristiche dei linguaggi di programmazione, ovvero delle lingue che utilizziamo per spiegare al computer cosa fare.

A che serve esattamente un linguaggio di programmazione e perché sono tanti?

Un computer è un simpatico ammasso di metallo, plastica, silicio e robaccia varia. Funziona grazie al fatto che una parte consistente della robaccia è in grado di produrre e leggere correnti elettriche. All’aumentare della complessità dei computer, è diventato piuttosto scomodo mettersi a far fare cose diverse al computer spostando fili da una parte all’altra, e si sono iniziati a creare componenti che smistano le correnti elettriche a seconda di quello che gli si dice “per iscritto” (attraverso passaggi che trasformavano lo scritto in cose più adatte ad essere lette a macchina).

Questo ha reso più facile spiegare cose ai computer, ed è stata universalmente considerata un’ottima idea. Dato però che il programmatore (all’epoca di solito la programmatrice) è sempre stato uno strano animale con un certo odio per le cose semplici, i computer sono diventati più complessi in modo che la difficoltà nello spiegargli cose non calasse troppo. Negli anni, quindi, abbiamo avuto modi sempre più semplificati per spiegare cose alle macchine, e macchine sempre più complesse e potenti a cui spiegarle.

I linguaggi di programmazione, insomma, servono a semplificare l’uso delle macchine, e sono molti perché complicazioni successive delle macchine hanno portato a semplificazioni successive del modo di usare le macchine senza dover essere eccezionalmente bravi. A questo si è “recentemente” aggiunto il fatto che macchine sempre più complesse sono adatte a fare così tante cose diverse da rendere necessario lo sviluppo di linguaggi per fargli fare cose specifiche (o addirittura per governare cose specifiche: un computer per giocare e uno per governare un sistema di radar non sono esattamente lo stesso oggetto e parlano lingue diverse già alla base).

Basso livello e alto livello

Questo tipo di evoluzione e questa complessità ha portato a creare linguaggi molto diversi fra loro prima di tutto nel livello di complessità.

I linguaggi più vicini possibile all’oggetto che devono programmare tendono a tradurre ogni singola operazione che l’oggetto può fare in un comando. Questo tipo di linguaggi si chiamano assembly e hanno la caratteristica di fornire il controllo completo dell’oggetto che governano, ma anche quella di essere estremamente legati ad esso (ogni oggetto avrà il suo assembly) e di essere molto difficili da utilizzare (va scritta nel dettaglio ogni singola procedura da fare). Linguaggi di questo tipo sono detti “di basso livello”, non perché siano stupidi o secondari, ma perché molto molto vicini all’oggetto.

Esistono poi una buona quantità di linguaggi di livello intermedio, ma comunque piuttosto basso. Di solito forniscono procedure già (ben) confezionate che permettono di evitare di dover scrivere per filo e per segno cose comuni, ma espongono ancora molto le caratteristiche – astraendole, ma poco – dell’hardware che programmano. Il vantaggio di questo tipo di linguaggi è il fornire ancora un certo controllo dell’hardware sottostante, ma mantenendosi molto indipendenti da esso. Due diversi hardware potrebbero essere programmabili nello stesso linguaggio senza eccessivi problemi. Il linguaggio principe appartenente a questa categoria è probabilmente il C.

Si può infine salire sempre più di livello, allontanandosi sempre di più dall’hardware e avvicinandosi al campo applicativo. Linguaggi di livello molto alto hanno l’enorme pregio di essere quasi completamente indipendenti dall’hardware sottostante, ma anche il difetto di perderne completamente il controllo. Inoltre, tendono ad essere abbastanza settoriali o generici al punto da essere scomodi in qualsiasi settore. Se ti allontani dall’hardware, infatti, devi avvicinarti a qualcosa: fare bene tutto ad alto livello è uno sforzo enorme, andrebbero scritte funzioni generiche per fare una quantità infinita di cose. I linguaggi di livello molto molto alto, quindi, sono quasi sempre specifici per fare determinate cose e funzionano male per farne altre. Sono, quindi, TANTISSIMI.

Compilati, interpretati e vie di mezzo

L’hardware non parla mai i linguaggi di programmazione. Questi quindi vanno sempre, in qualche modo, tradotti in qualcosa che l’hardware capisca.

Inizialmente, con i linguaggi assembly, esisteva un software chiamato assembler che si occupava di tradurre le istruzioni scritte in lingua umanamente comprensibile nelle rispettive istruzioni scritte nella reale lingua dell’hardware. Tale operazione era abbastanza semplice: come abbiamo detto, a ogni istruzione del linguaggio corrispondeva (grossomodo) una istruzione dell’hardware.

Quando le cose si sono complicate con l’arrivo dei linguaggi di alto livello, si sono introdotti dei software piú complessi chiamati compilatori. Il compilatore fa un’operazione molto simile a quella dell’assembler, ma il suo compito è piú difficile: ogni istruzione del linguaggio va tradotta in una serie di istruzioni nel modo migliore possibile, sia come fedeltà sia soprattutto come ottimizzazione (la stessa cosa, come scrivere un file sul disco, si può fare in molti modi diversi e va scelto il migliore). Va da sé che il compilatore deve essere specifico per l’hardware, perché ogni hardware ha un suo set di istruzioni, mentre il linguaggio è sempre il medesimo.

Con l’evoluzione delle macchine, specialmente per quanto riguarda la velocità, sono nati alcuni linguaggi che invece si dicono “interpretati”, e non hanno un vero e proprio compilatore, ma un interprete. Un interprete, semplificando un po’, è un oggetto – sempre specifico per l’hardware – che legge il programma scritto in linguaggio e lo traduce in linguaggio dell’hardware mentre lo esegue. In questo modo non è necessario compilare prima di eseguire. Il prezzo è quello di consumare più risorse, con il vantaggio di semplificare di molto sia le operazioni di sviluppo che quelle di debugging. Nascono come linguaggi interpretati moltissimi dei linguaggi molto diffusi oggi: Python, PHP, JavaScript…

Esistono ovviamente molti approcci ibridi.

In diversi casi (notissimo quello di Java, ma vi sono esempi meno recenti come LISP), l’interprete non esegue il codice scritto dal programmatore, ma un linguaggio dell’interprete stesso, chiamato bytecode. Questo richiede un passo di compilazione, dal linguaggio al bytecode, ma mantiene il vantaggio della possibilità di essere eseguito senza ulteriori passi di compilazione su qualunque macchina su cui giri l’interprete. Un linguaggio compilato per un hardware, invece, non girerebbe su un hardware diverso.

Può anche avvenire, specie nei linguaggi di questo genere, che alcune istruzioni molto usate vengano compilate (just in time compilation) dall’interprete in linguaggio macchina e salvate per essere pronte all’uso ogni volta che servono, mentre il resto del codice viene compilato in bytecode o interpretato direttamente ad alto livello. Quasi tutti i linguaggi che utilizzano interpreti di bytecode molto vicini alla macchina, come le versioni recenti di Java, diversi dialetti di LISP, Swift, e di recentissimo persino PHP, supportano questo tipo di approccio.

I paradigmi

Come ultima vera e propria caratteristica dei linguaggi, mi tengo la più complessa da spiegare, ovvero i paradigmi.

I linguaggi sono formati da elementi che tipicamente sono comuni a circa tutti (istruzioni, variabili), elementi comuni a molti (funzioni) e elementi comuni a pochi (classi, oggetti, moduli…). Il modo in cui queste cose si usano e si fanno interagire tra loro si chiama paradigma di programmazione.

I paradigmi sono veramente moltissimi, ed è estremamente complicato sia elencarli, sia spiegarli in maniera generale. Ne elencherò quindi alcuni tra quelli più comuni oggi, spiegando come sono fatti e a cosa servono. Una cosa importante da capire prima di cominciare è che oggi di rado un linguaggio è utilizzabile con un solo paradigma. Molto spesso i linguaggi, pur essendo più adatti a un paradigma, sono molto flessibili a riguardo.

La programmazione strutturata

Non si può parlare dei paradigmi senza spiegare prima quello che è padre di molti altri, la programmazione strutturata.

Un programma strutturato esegue le istruzioni che lo compongono una dopo l’altra, in rigida sequenza. Nella sequenza è però possibile inserire due tipi di strutture di controllo.

La prima si chiama selezione (tipicamente l’istruzione if, che significa “se”), che permette di saltare una sequenza di istruzioni se non viene verificata una condizione:

 variabile a = NumeroACaso
 variabile b = NumeroACaso
 variabile c = 0;
 ​
 if (a > b) {
   c = a - b
 }
 if (a < b) {
   c = b - a
 }
 ​
 a questo punto c vale la differenza senza segno tra a e b

La seconda si chiama iterazione (le istruzioni for, while, until e molte altre), ed è molto simile ma opposta: esegue un blocco di codice finché una certa condizione resta vera. Evito l’esempio perché ci sono davvero tanti modi di farlo e sarebbe fuorviante, ma il concetto dovrebbe essere piuttosto chiaro.

Questo paradigma di programmazione caratterizza fortissimamente i linguaggi antichi ma non antichissimi, come il C (prima esisteva la programmazione procedurale, che vi risparmio e che è molto simile ma con le strutture “fatte a mano” e quindi lasche e incasinabili), e tutti i linguaggi che da questi derivano. Se ne trovano tracce in qualsiasi linguaggio molto diffuso oggi. Sono davvero pochi i linguaggi che non supportano un minimo di approccio strutturato, anche quando andrebbe evitato.

La programmazione a oggetti

Il paradigma forse oggi più generalmente famoso – nonostante il suo uso in maniera pura stia scemando – è quello della programmazione a oggetti.

Tale paradigma riprende gran parte dei concetti nati nell’ambito della programmazione strutturata, ma introduce il concetto di oggetto. Un oggetto è un elemento complesso in grado di possedere uno stato (una serie di proprietà) e di comunicare dati con altri oggetti, modificando il proprio stato e il loro.

Detta così è molto complicata, e fare un esempio come codice sarebbe molto lungo per questo articolo, ma l’idea di base è semplice. In un linguaggio a oggetti posso avere un oggetto aula e un oggetto per ognuno degli studenti di una scuola. Nel normale svolgimento del programma, può avvenire che uno studente viene fatto entrare nell’aula. Quando avviene che Mario Rossi entra nell’aula, Mario Rossi potrà cambiare il suo stato annotando “sono nell’aula tale”, e aggiornare lo stato dell’aula aggiungendo al registro delle presenze, presente nello stato dell’aula, il fatto che c’è anche lui.

Il flusso del programma, quindi, non scorre piú linearmente. Alle volte si finisce per uscire dal flusso principale e finire nel flusso interno di un oggetto per variarne lo stato. In un paradigma fortemente a oggetti, questo avviene molto spesso, complicando la possibilità del programmatore di avere il controllo dell’intero flusso – e quindi complicando il debug – ma semplificando di molto la rappresentazione di una gran quantità di problemi.

Gli esempi più rudimentali di linguaggi che hanno usato il paradigma a oggetti sono direttissimi discendenti del C, come ObjectiveC e C++. Oggi sono disponibili linguaggi che supportano il paradigma a oggetti anche molto moderni, anagraficamente o concettualmente, come Swift, C#, Python, Java (moderno per modo di dire), derivati di Java (Kotlin…), PHP e, per finta ma ci prova, anche un pochino JavaScript.

La programmazione funzionale

La programmazione funzionale è un paradigma di programmazione che oggi è di gran moda, sebbene non molta gente pare averlo compreso a fondo. È molto curiosamente la diretta trasposizione in informatica di un paradigma di calcolo matematico, il lambda calcolo, che originariamente non era stato pensato per essere usato dai computer, ma che si è dimostrato molto adatto.

Nella programmazione funzionale, il mattone di base del programma non sono le istruzioni ma le funzioni, intese in senso strettissimamente matematico: una funzione è una cosa che, prese delle informazioni in ingresso restituisce delle informazioni in uscita, sempre le stesse se l’ingresso è sempre lo stesso, e senza che avvenga alcun altro effetto collaterale. Una parte consistente di quelle che gli altri linguaggi chiamano “funzioni”, quindi, non sono considerabili tali nell’ambito dei linguaggi funzionali.

Il paradigma funzionale deve veramente poco al paradigma strutturato, e quindi risulta quasi sempre parecchio difficile da comprendere. In particolare, tipicamente, manca del concetto di “memoria” e di “variabile” (entrambe le cose sono sostituite dalle funzioni e dai loro nomi), e manca del concetto di iterazione – quasi impossibile senza variabili – sostituito da quello di ricorsione (funzioni che richiamano sé stesse).

A lungo considerati linguaggi interessanti, specie in applicazioni matematiche che vengono riportate molto fedelmente in linguaggio, ma inutilizzabili perché scomodi da leggere e soprattutto esosi di risorse (la ricorsione è il male per i computer), sono oggi diventati estremamente popolari sia perché sono stati fatti grandi miglioramenti nel tempo, sia perché i computer oggi sono sufficientemente potenti da non avere grandi problemi nel compilarli ed eseguirli.

Il linguaggio funzionale diffuso più antico è il reverendissimo LISP, e tra i matematici è stato popolare Mathematica, ma il successo recente è dovuto molto a Scala e al fatto che molti linguaggi abbiano reso possibile ampio uso di questo paradigma: Ruby, PERL, Python e di recente JavaScript.

La programmazione a eventi

Il paradigma a eventi viene spesso affiancato a uno qualsiasi degli altri paradigmi, e consiste ai fatti in moltissimi micro-programmi, detti handler, che possono appunto a loro volta usare un paradigma qualunque, e che vengono eseguiti solo se si verifica una certa condizione.

Un programma a eventi esegue di continuo, ripetutamente, una procedura che controlla che eventi sono avvenuti dopo l’ultimo giro ed esegue i sottoprogrammi che vanno eseguiti. Il flusso del programma non è quindi in alcun modo lineare, e nessun sottoprogramma può essere certo che gli altri vengano eseguiti (ma può generare eventi in maniera che vengano eseguiti entro il prossimo giro della routine principale).

L’esempio più diffuso di programmazione a eventi è il JavaScript utilizzato all’interno dei browser. Consiste in molte routine che si attivano solo se succede qualcosa (“l’utente scrolla”, “l’utente clicca”). Abbiamo però, come utenti, di continuo a che fare con un altro, enorme, programma ad eventi: il sistema operativo. Ne parleremo in un prossimo articolo di questa rubrica.

Il resto

La lista non si esaurisce qui, ma il grosso degli altri paradigmi sono o minoritari, o superati, o estremamente specifici (amo molto il paradigma pattern-matching, usato da linguaggi di programmazione considerabili pienamente tali ma che hanno come unico fine quello di trovare stringhe in un flusso).

Se qualcuno di voi fosse esperto di altri paradigmi, vi invito a integrare nei commenti al thread dedicato a questo articolo su /r/ItalyInformatica

I framework

In ultimo, parliamo dei framework, cosa con cui si ha a che fare sempre più spesso.

C’è un limite a quanto di alto livello può essere un linguaggio senza essere iperdispersivo. Come dicevamo, più ti allontani dall’hardware, più diventa impossibile coprire tutto. Il grosso dei linguaggi di alto livello, persino quelli molto specifici come il PHP o il JavaScript che nascono per essere utilizzati in ambito web, sono linguaggi ad uso molto generico. Fanno un po’ di tutto, male e niente molto bene.

Per questo motivo, si sono a un certo punto diffusi i framework. Un framework è un grosso insieme di funzioni e strutture pronte per un certo linguaggio di programmazione, che lo specializza in un preciso settore e lo alza di livello.

Se PHP è un linguaggio molto orientato al web ma ancora abbastanza generico, Laravel è un suo framework di livello più alto, con forte vocazione a oggetti e parzialmente funzionale, pensato solo ed esclusivamente per fare gestionali CRUD per il web. Se Java è un linguaggio molto generico, Spring è un suo framework di alto livello per la realizzazione di sistemi web. Se Swift è un linguaggio generico, SwiftUI è un framework di alto livello per la scrittura di app iOS. Se Ruby è un linguaggio generico, Ruby on Rails è un framework di alto livello per sistemi web (sigh scusate, è il mio campo).

Insomma, un framework è un set di strumenti molto esteso – spesso quasi un linguaggio a parte – scritto però sulle spalle di un linguaggio già esistente.

Conclusioni

Ho faticato un po’ a fare una cosa coerente e con un minimo di filo. Spero abbia sufficiente comprensibilità e un minimo di senso. Chiedete pure ulteriori dettagli, che è un argomento che merita facilmente un secondo articolo.