La “vera” informatica

Documento di Briefing: Principi Fondamentali dell’Informatica e Progettazione del Software

Questo documento di briefing riassume i concetti chiave e le metodologie discusse negli estratti forniti dal testo “Structure and Interpretation of Computer Programs” (SICP), con particolare attenzione ai principi di astrazione, modularità, gestione dello stato e progettazione del linguaggio.

1. Costruire Astrazioni con Procedure

Il fondamento della programmazione, come illustrato nel testo, è la capacità di costruire astrazioni. Un linguaggio di programmazione potente non è solo un mezzo per istruire un computer, ma un “nuovo mezzo formale per esprimere idee sulla metodologia.”

1.1. Gli Elementi della Programmazione

Un linguaggio di programmazione potente si basa su tre meccanismi fondamentali per combinare idee semplici e formarne di più complesse:

  • Espressioni primitive: “rappresentano le entità più semplici di cui il linguaggio si occupa”.
  • Mezzi di combinazione: consentono di costruire espressioni composte da quelle primitive.
  • Mezzi di astrazione: permettono di nominare e manipolare entità composte come unità singole.

Lisp, e il suo dialetto Scheme utilizzato nel libro, eccelle in questi aspetti grazie alla sua semplicità sintattica e semantica. La definizione è il mezzo più semplice di astrazione, consentendo di associare nomi a valori. Le procedure composte offrono un mezzo di astrazione molto più potente, permettendo di dare un nome a un’operazione composta e riferirvisi come un’unità.

1.2. Processi Generati dalle Procedure

Le procedure non sono solo “pattern per l’evoluzione locale di un processo computazionale”, ma specificano anche il comportamento globale del processo. Il testo distingue tra:

  • Processi Ricorsivi Lineari: Caratterizzati da una “forma di espansione seguita da contrazione”, che accumula una catena di operazioni differite. Richiede spazio e passi proporzionali a n (Θ(n)).
  • Processi Iterativi Lineari: Richiedono spazio costante (Θ(1)) e passi proporzionali a n (Θ(n)), anche se descritti da una procedura ricorsiva in linguaggi “tail-recursive” come Scheme.
  • Ricorsione ad Albero: Processi in cui i rami si dividono a ogni livello, come nel calcolo dei numeri di Fibonacci con una definizione ingenua. Questi processi possono richiedere un numero di passi che “cresce esponenzialmente con l’input” (Θ(ϕn)), anche se lo spazio richiesto cresce linearmente (Θ(n)).

Il concetto di ordine di crescita (Θ(f(n))) è introdotto come una “misura grossolana delle risorse richieste da un processo man mano che gli input diventano più grandi”. Questo permette di confrontare l’efficienza degli algoritmi, come l’esponenziazione logaritmica (Θ(logn)) rispetto all’esponenziazione lineare (Θ(n)).

1.3. Formulazione di Astrazioni con Procedure di Ordine Superiore

Le procedure di ordine superiore consentono di esprimere metodi generali di calcolo, indipendentemente dalle funzioni particolari coinvolte. Esempi includono la procedura sum per la sommatoria di serie e la procedura integral per l’integrazione numerica.

  • Procedure come Argomenti: Le procedure possono essere passate come argomenti ad altre procedure, permettendo la creazione di funzioni più generiche come sum o accumulate.
  • lambda per Procedure Anonime: La forma speciale lambda permette di creare procedure senza associarle a un nome nell’ambiente, riducendo l’ingombro di definizioni di procedure “banali”.
  • let per Variabili Locali: let è una forma speciale (zucchero sintattico) per creare variabili locali, equivalente all’applicazione di una procedura lambda anonima.
  • Procedure come Valori Restituiti: Le procedure possono anche essere restituite come valori da altre procedure, consentendo la creazione di trasformazioni generiche, come average-damp per migliorare la convergenza dei metodi di punto fisso, o newton-transform per il metodo di Newton.

L’uso di procedure di ordine superiore permette ai programmatori di “manipolare questi metodi generali per creare ulteriori astrazioni”, elevando il livello di pensiero nella progettazione del software.

2. Costruire Astrazioni con Dati Composti

Oltre a combinare procedure, un aspetto fondamentale di qualsiasi linguaggio di programmazione è la capacità di costruire astrazioni “combinando oggetti dati per formare dati composti.”

2.1. Astrazione dei Dati

L’astrazione dei dati è una metodologia cruciale per la strutturazione dei sistemi. L’idea centrale è “strutturare i programmi che devono utilizzare oggetti dati composti in modo che operino su ‘dati astratti’.” Questo significa isolare l’uso di un oggetto dati dalle sue implementazioni concrete.

  • Barriere di Astrazione: Il sistema di numeri razionali è utilizzato come esempio per illustrare le barriere di astrazione, dove le procedure di alto livello (es. add-rat) utilizzano selettori e costruttori astratti (numer, denom, make-rat), che a loro volta sono implementati in termini di strutture primitive (cons, car, cdr).
  • Coppie come Elementi Fondamentali: La procedura cons e i selettori car e cdr sono i “soli elementi di collegamento di cui abbiamo bisogno” per costruire strutture dati complesse.
  • Significato dei Dati: I dati sono definiti da una “collezione di selettori e costruttori, insieme a condizioni specificate che queste procedure devono soddisfare per essere una rappresentazione valida”. Sorprendentemente, cons, car e cdr possono essere implementati usando solo procedure, senza alcuna struttura dati esplicita, evidenziando la natura procedurale dei dati.

2.2. Strutture Gerarchiche e Operazioni sulle Sequenze

Le coppie possono essere utilizzate per costruire sequenze (collezioni ordinate di oggetti dati) e strutture gerarchiche (sequenze i cui elementi possono essere essi stessi sequenze), come gli alberi.

  • Proprietà di Chiusura di cons: La capacità di creare coppie i cui elementi sono coppie è “l’essenza dell’importanza delle strutture di lista come strumento di rappresentazione”. Questa proprietà di chiusura è fondamentale per la potenza di qualsiasi mezzo di combinazione.
  • Operazioni sulle Sequenze come Interfacce Convenzionali: Le liste possono fungere da “interfaccia convenzionale” per connettere moduli di elaborazione, rendendo i programmi più modulari e leggibili. Procedure di ordine superiore come map, filter e accumulate (“fold-right”) operano su queste sequenze, permettendo di esprimere “piani di flusso del segnale” dove i dati fluiscono attraverso una cascata di stadi.
  • Esempio di Linguaggio per Immagini: Un linguaggio semplice per disegnare immagini dimostra ulteriormente il potere dell’astrazione dei dati e della chiusura, dove gli oggetti dati combinati sono rappresentati come procedure. Questo è un esempio di progettazione stratificata, in cui un sistema complesso è strutturato come una sequenza di livelli, ciascuno con il proprio linguaggio di primitivi, mezzi di combinazione e astrazione.

2.3. Dati Simbolici

I dati simbolici estendono la potenza rappresentativa del linguaggio permettendo agli elementi di base di essere simboli arbitrari anziché solo numeri.

  • Espressioni Algebriche: Le espressioni algebriche possono essere rappresentate come liste di simboli, consentendo la manipolazione simbolica, come la differenziazione. L’astrazione dei dati permette di definire una procedura di differenziazione generica che funziona indipendentemente dalla rappresentazione interna delle espressioni algebriche.
  • Insiemi come Liste Ordinate o Alberi Binari: Gli insiemi possono essere rappresentati in vari modi (liste non ordinate, liste ordinate, alberi binari), ciascuno con diverse implicazioni sull’efficienza delle operazioni (ad es., Θ(n) per liste ordinate rispetto a Θ(logn) per alberi bilanciati).
  • Alberi di Huffman: Gli alberi di Huffman sono un esempio sofisticato di struttura dati per la codifica, in cui la rappresentazione interna (nodi foglia o nodi generali) richiede procedure generiche che si adattano al tipo di dato.

2.4. Rappresentazioni Multiple per Dati Astratti

I sistemi complessi spesso richiedono la gestione di tipi di dati con più rappresentazioni, come i numeri complessi (forma rettangolare e polare). La programmazione guidata dai dati (data-directed programming) è una tecnica chiave per gestire questa complessità.

  • Etichette di Tipo (Type Tags): Ogni rappresentazione di un tipo di dato viene etichettata, consentendo alle procedure di “dispatchare” all’operazione corretta in base al tipo di dato. Questo evita di dover modificare le procedure esistenti quando si aggiungono nuove rappresentazioni.

2.5. Operazioni Aritmetiche Generiche

Il sistema di aritmetica generica estende ulteriormente l’idea della programmazione guidata dai dati per gestire operazioni aritmetiche su tipi di numeri diversi (numeri ordinari, razionali, complessi, polinomi).

  • Coercizione: Quando si combinano tipi diversi, la coercizione converte un oggetto di un tipo in un oggetto equivalente di un altro tipo (ad es., un numero ordinario in un numero complesso).
  • Gerarchie di Tipi (Torri): I tipi possono essere organizzati in una gerarchia (come interi -> razionali -> reali -> complessi), dove ogni tipo eredita le operazioni del suo supertipo. Questo semplifica l’aggiunta di nuovi tipi e permette al sistema di “alzare” i tipi inferiori per farli operare allo stesso livello.
  • Aritmetica sui Polinomi: La gestione dei polinomi, i cui coefficienti possono essere di qualsiasi tipo numerico supportato dal sistema aritmetico generico, è un esempio di come l’astrazione dei dati e le operazioni generiche consentano la costruzione di sistemi potenti e flessibili.

3. Modularità, Oggetti e Stato

Il Capitolo 3 introduce il concetto di stato mutabile e la assegnazione (set!) come meccanismo fondamentale per modellare oggetti che cambiano nel tempo, in particolare nella simulazione di sistemi fisici.

3.1. Assegnazione e Variabili Locali

L’assegnazione permette di creare oggetti con “stato locale”, dove il valore di una variabile può cambiare nel tempo.

  • Costo dell’Assegnazione: L’introduzione dell’assegnazione comporta una perdita del modello di sostituzione per l’applicazione di procedure. Un programma con assegnazione non può più essere interpretato semplicemente sostituendo i parametri formali con gli argomenti, perché il significato di una variabile ora dipende da un “luogo” in cui i valori possono essere memorizzati.
  • Identità e Cambiamento: Gli oggetti con stato mutabile acquisiscono un’identità distinta dalle loro parti. Due oggetti possono avere lo stesso stato ma essere distinti se le operazioni su di essi possono avere effetti diversi.

3.2. Il Modello Ambientale di Valutazione

Il modello ambientale sostituisce il modello di sostituzione per descrivere l’applicazione di procedure in presenza di assegnazione.

  • Ambienti e Frame: Un ambiente è una sequenza di frame, e ogni frame è una tabella di associazioni che legano nomi di variabili a valori. Ogni frame punta anche al suo ambiente di inclusione.
  • Procedure come Coppie Codice-Ambiente: Una procedura è sempre una coppia composta da un po’ di codice e un puntatore all’ambiente in cui la procedura è stata creata.
  • Definizioni Interne: Le definizioni interne creano una struttura a blocchi, dove le variabili definite all’interno di una procedura hanno uno scope locale a quella procedura.

3.3. Dati Mutabili

Le operazioni set-car! e set-cdr! consentono di modificare le coppie esistenti, permettendo la costruzione di strutture dati che non possono essere create solo con cons, car e cdr.

  • Condivisione: La condivisione di strutture dati mutabili (alias) può avere conseguenze inaspettate quando gli oggetti vengono modificati.
  • Code (Queues): Le code sono sequenze in cui gli elementi possono essere inseriti alla fine ed eliminati dall’inizio. Una rappresentazione efficiente utilizza una coppia di puntatori (fronte e coda) per la lista sottostante, consentendo operazioni in tempo costante (Θ(1)).
  • Tabelle: Le tabelle associano chiavi a valori. Possono essere implementate come liste di record (coppie chiave-valore) o proceduralmente come oggetti con stato locale che gestiscono una tabella interna.
  • Memorizzazione (Memoization): Una tecnica che permette a una procedura di registrare in una tabella locale i valori precedentemente calcolati per evitare computazioni ridondanti, migliorando significativamente le prestazioni.
  • Simulatore di Circuiti Digitali: Un esempio di sistema di simulazione basato su eventi che modella circuiti digitali con oggetti computazionali (fili, porte) che interagiscono attraverso segnali. Questo illustra come la progettazione orientata agli oggetti con stato locale sia potente per modellare sistemi fisici.
  • Propagazione dei Vincoli: Un linguaggio che permette di esprimere relazioni tra quantità, anziché solo computazioni unidirezionali. Le reti di vincoli sono costruite con connettori che “tengono” valori e vincoli primitivi che specificano le relazioni.

3.4. Concorrenza: Tempo e Stato Condiviso

L’introduzione dell’assegnazione e dello stato mutabile diventa particolarmente problematica in presenza di concorrenza, dove più processi possono essere eseguiti contemporaneamente.

  • Problemi di Concorrenza: Le operazioni concorrenti su variabili di stato condivise possono portare a risultati indeterminati e incorretti, a causa di timing non specificati (race conditions).
  • Serializzazione: La serializzazione è un meccanismo per controllare l’accesso a risorse condivise, garantendo che le operazioni su uno stato condiviso avvengano atomicamente (come se fossero eseguite sequenzialmente). I serializer (mutex) vengono utilizzati per proteggere le parti critiche del codice.

3.5. Stream: L’Astrazione del Tempo

Gli stream offrono un approccio alternativo alla modellazione dello stato, mitigando alcune delle complessità introdotte dall’assegnazione. Invece di aggiornare variabili di stato, lo stato è rappresentato come una “sequenza di valori senza tempo”.

  • Valutazione Pigra (Lazy Evaluation): Gli stream sono implementati utilizzando la valutazione pigra, dove i valori vengono calcolati solo quando sono effettivamente necessari. Ciò permette di rappresentare “liste infinite” di dati, come i numeri interi o di Fibonacci.
  • delay e force: Le forme speciali delay e cons-stream (che utilizza delay) creano una “promessa” di calcolare il resto di uno stream solo quando viene richiesto (force). L’ottimizzazione della memorizzazione (memoization) dei thunks (memo-proc) evita computazioni ridondanti.
  • Stream come Segnali: Gli stream possono modellare direttamente i sistemi di elaborazione del segnale, rappresentando i valori di un segnale in intervalli di tempo successivi. Questo permette di costruire sistemi con confini modulari diversi rispetto ai sistemi organizzati attorno all’assegnazione di variabili di stato.
  • Formulazione di Iterazioni come Processi Stream: Le iterazioni possono essere espresse come stream di stati, dove ogni elemento dello stream è lo stato del sistema in un dato momento. Questo permette tecniche avanzate come l’accelerazione di sequenze (es. trasformazione di Eulero) manipolando l’intera sequenza di stati come una struttura dati.
  • Vista Funzionale del Tempo: Gli stream offrono una “vista funzionale del tempo”, dove il comportamento di un sistema può essere descritto come una funzione matematica i cui valori non cambiano. La percezione dell’utente di un sistema con stato che cambia è un’imposizione temporale. Questo approccio è particolarmente attraente per i sistemi concorrenti, in quanto elimina le problematiche legate all’ordine degli eventi e alla sincronizzazione.

4. Meta-Linguaggi e Valutatori

Il Capitolo 4 esplora come i linguaggi di programmazione possono essere stabiliti e implementati in termini di altri linguaggi, in particolare utilizzando Lisp come base per costruire valutatori.

4.1. Il Valutatore Metacircolare

Un valutatore metacircolare è un interprete per un linguaggio di programmazione scritto nel linguaggio stesso. Il valutatore descritto è un sottoinsieme di Scheme.

  • Struttura del Valutatore: Il valutatore ha due parti principali: eval che classifica le espressioni in base al loro tipo e apply che gestisce l’applicazione di procedure agli argomenti.
  • Tipi di Espressioni: Le espressioni sono classificate come auto-valutanti (numeri, stringhe), variabili, espressioni quotate, assegnazioni, definizioni, condizionali (if, cond), espressioni lambda e applicazioni di procedure.
  • Modello Ambientale: Il valutatore utilizza il modello ambientale per gestire le variabili e gli ambienti, mantenendo una distinzione chiara tra l’ambiente in cui una procedura viene definita e l’ambiente in cui viene applicata.
  • Sintassi Astratta: Le operazioni del valutatore operano su una rappresentazione di “sintassi astratta” delle espressioni, isolando l’interprete dai dettagli della sintassi concreta.
  • Espressioni Derivate: Alcune forme speciali (come cond o let) possono essere definite in termini di altre forme speciali, semplificando l’implementazione del valutatore.
  • Definizioni Interne: La gestione delle definizioni interne è complessa, in particolare per la ricorsione mutua, e richiede un’attenta considerazione dello scope delle variabili. L’implementazione prevede la scansione delle definizioni interne e la loro trasformazione in assegnazioni a variabili inizialmente “non assegnate”.

4.2. Variazioni su Scheme – Valutazione Pigra

La valutazione pigra è una modifica al modello di valutazione di Scheme, dove le procedure composte sono “non-strict” nei loro argomenti (ovvero, gli argomenti non vengono valutati finché i loro valori non sono necessari).

  • Thunks: Gli argomenti ritardati vengono trasformati in oggetti chiamati “thunks”, che incapsulano l’espressione e l’ambiente in cui deve essere valutata. La forzatura (forcing) di un thunk produce il suo valore.
  • Memoization dei Thunks: I thunks possono essere memorizzati, in modo che il loro valore venga calcolato solo una volta e poi riutilizzato.
  • Stream come Liste Pigre: Con la valutazione pigra, cons può essere reso non-strict, permettendo di trattare gli stream come liste ordinarie, eliminando la necessità di forme speciali come cons-stream e procedure di manipolazione di stream separate.

4.3. Variazioni su Scheme – Calcolo Non-Deterministico

Il calcolo non-deterministico estende il valutatore di Scheme per supportare la ricerca automatica, dove le espressioni possono avere più di un valore possibile.

  • amb e Ricerca: La forma speciale amb restituisce “ambiguamente” il valore di una delle espressioni fornite. Il valutatore amb esplora sistematicamente i percorsi di esecuzione, facendo il backtracking quando incontra un vicolo cieco.
  • Continuazioni di Successo e Fallimento: L’implementazione del valutatore amb si basa sull’uso di continuazioni di successo (che gestiscono il valore corrente e la prossima continuazione di fallimento) e continuazioni di fallimento (che gestiscono il backtracking).
  • Esempi di Programmi Non-Deterministici: Il calcolo non-deterministico è utile per “generare e testare” applicazioni, come la risoluzione di puzzle logici o l’analisi sintattica del linguaggio naturale. L’assegnazione in un contesto non-deterministico richiede un meccanismo per annullare gli effetti collaterali durante il backtracking.

4.4. Programmazione Logica

La programmazione logica estende l’idea del calcolo non-deterministico combinando una visione relazionale della programmazione con una potente forma di pattern matching simbolico chiamata unificazione.

  • Recupero Deductivo delle Informazioni: La programmazione logica è particolarmente adatta per la gestione di basi di dati e il recupero di informazioni. Il linguaggio di query implementato permette l’accesso basato su pattern alle informazioni e la deduzione logica basata su regole.
  • Basi di Dati e Query: Una base di dati è una collezione di asserzioni. Le query utilizzano variabili pattern (es. ?x) per trovare tutte le istanze che soddisfano un dato pattern. Le query possono essere composte con and, or, not e lisp-value.
  • Regole: Le regole sono un mezzo di astrazione per le query, specificando che una conclusione vale se un corpo è soddisfatto. Possono essere ricorsive e permettono la deduzione logica.
  • Unificazione: L’unificazione è il processo che cerca di rendere uguali due pattern (che possono contenere variabili) assegnando valori alle variabili.
  • Problemi con not: L’operatore not nella programmazione logica ha delle peculiarità; “not P” significa che P non è deducibile dalla base di dati (assunzione del mondo chiuso), che è diverso dalla negazione logica standard. Questo può portare a risultati inaspettati a seconda dell’ordine delle clausole nelle query.
  • Implementazione: L’interprete del linguaggio di query è implementato utilizzando flussi (stream) di frame, dove ogni frame rappresenta un insieme di legami di variabili. I processi di query sono concepiti come filtri e trasformatori di questi flussi.

5. Calcolo con Macchine a Registri

Il Capitolo 5 descrive come i programmi Scheme possono essere tradotti in descrizioni di macchine a registri, fornendo un modello esplicito dei meccanismi di controllo e gestione degli argomenti nel processo di valutazione.

5.1. Linguaggio per Descrivere Macchine a Registri

Un linguaggio testuale viene introdotto per descrivere le macchine a registri, che sono “dispositivi che combinano un insieme di dati con un programma che opera su tali dati”.

  • Elementi di una Macchina a Registri:Registri: Memorie locali per memorizzare valori.
  • Unità Operazionali: Componenti hardware che eseguono operazioni (es. +, rem).
  • Data Path: La rete di connessione tra registri e unità operazionali.
  • Controller: L’unità di controllo che dirige la sequenza di operazioni.
  • Processi Iterativi e Ricorsivi: Le macchine a registri possono implementare sia processi iterativi (looping) che processi ricorsivi (chiamate a subroutine). La ricorsione richiede un stack per salvare i valori dei registri e gli indirizzi di ritorno, poiché la profondità delle chiamate può essere arbitraria.

5.2. Simulatore di Macchine a Registri

Un simulatore Scheme viene costruito per eseguire le descrizioni delle macchine a registri.

  • Struttura del Simulatore: Il simulatore rappresenta i registri, lo stack, le unità operazionali e il controller come procedure con stato locale.
  • Assemblatore: Un assemblatore traduce la sequenza di istruzioni testuali in procedure di esecuzione che possono essere chiamate dal controller.
  • Monitoraggio delle Prestazioni: Il simulatore può essere esteso per monitorare le prestazioni della macchina simulata, ad esempio contando le operazioni dello stack o il numero di istruzioni eseguite.

5.3. Implementazione della Struttura delle Liste

Le macchine a registri hanno bisogno di un modo per implementare strutture dati come le liste.

  • Memoria e Puntatori: La memoria del computer è modellata come vettori (the-cars, the-cdrs) dove un puntatore a una coppia è un indice in questi vettori. I dati sono rappresentati con puntatori tipati per distinguere i diversi tipi di dati (numeri, simboli, coppie).
  • Gestione della Memoria (Garbage Collection): Per affrontare la memoria finita, viene introdotto un sistema di raccolta dei rifiuti (garbage collection). Il metodo “stop-and-copy” sposta i dati “utili” da una parte della memoria all’altra, liberando la memoria occupata dai dati “spazzatura”.

5.4. Il Valutatore a Controllo Esplicito

Il valutatore metacircolare (Capitolo 4) viene trasformato in una descrizione di macchina a registri, il valutatore a controllo esplicito. Questo mostra come i meccanismi di chiamata di procedura e passaggio di argomenti siano implementati in termini di operazioni su registri e stack.

  • Registri e Operazioni: Il valutatore utilizza registri per tenere traccia dello stato (ambiente, espressione corrente, valore, puntatore del programma, stack) e operazioni primitive che corrispondono alle procedure di sintassi astratta e di manipolazione dell’ambiente.
  • Flusso di Controllo: Il controller del valutatore implementa le regole di valutazione di eval e apply come una sequenza di istruzioni, gestendo esplicitamente le chiamate ricorsive e lo stack.
  • Tail Recursion (Ricorsione di Coda): Il valutatore è progettato per supportare la ricorsione di coda, un’ottimizzazione che permette ai processi iterativi implementati con ricorsione di utilizzare spazio costante nello stack.
  • Loop Read-Eval-Print: Il valutatore include un ciclo Read-Eval-Print che interagisce con l’utente.

5.5. Compilazione

La compilazione è un processo che traduce programmi da un linguaggio di alto livello a un linguaggio di macchina. Un compilatore è una procedura che “pre-analizza” le espressioni del programma e genera codice macchina specifico per quelle espressioni, che è più efficiente dell’esecuzione interpretata.

  • Struttura del Compilatore: Il compilatore genera sequenze di istruzioni per la macchina a registri. Utilizza meccanismi come append-instruction-sequences e preserving per combinare sequenze di istruzioni in modo efficiente, ottimizzando l’uso dello stack.
  • Generazione di Codice: Il compilatore ha procedure dedicate per generare codice per i diversi tipi di espressioni (variabili, assegnazioni, condizionali, procedure lambda, chiamate di procedura).
  • Ricorsione di Coda: Il compilatore è progettato per generare codice tail-recursive, permettendo ai processi iterativi di essere eseguiti con un uso efficiente dello stack.
  • Indirizzamento Lessicale: Una tecnica di ottimizzazione che permette al compilatore di determinare l’indirizzo esatto delle variabili nell’ambiente in fase di compilazione, evitando la costosa ricerca run-time.
  • Integrazione di Codice Compilato e Interpretato: I sistemi reali spesso devono gestire l’interazione tra codice interpretato e codice compilato.

Lascia un commento