Anteprima della struttura

Quando una soluzione corretta è progettata male

Una lezione su IA, numeri casuali e metodo MOSAICO: da RandomNumero a RandomRange, per distinguere correttezza, riuso e responsabilità.

Articoli /quando-una-soluzione-corretta-e-progettata-male
Quando una soluzione corretta è progettata male

12 min

Salvatore Mosaico · 2026

Indice

Introduzione

Un programma può fornire sempre il risultato corretto ed essere comunque progettato male. Questa lezione mette in scena un dialogo tra docente, studente e intelligenza artificiale. Il percorso sembra semplice: usare Math.random() di JavaScript per generare numeri interi casuali. Il vero obiettivo emerge alla fine.

Nella prima giornata si costruisce una funzione generale che restituisce un intero da 1 a N. Nella seconda si estende il risultato a un intervallo [a,b]. L’IA fornisce una formula funzionante. Il docente sorprende la classe: la soluzione è matematicamente corretta, ma viola una scelta architetturale del progetto.

Occorre distinguere quattro livelli: validità sintattica, correttezza matematica, superamento dei test e coerenza progettuale. Quando una capacità è già stata isolata e verificata a un livello inferiore, il livello superiore deve utilizzarla invece di riprodurne la logica, se questo è il contratto del sistema.

Non basta ottenere il risultato giusto. Un buon programmatore deve collocare ogni responsabilità nel posto giusto. I dialoghi seguenti sono simulazioni didattiche, non trascrizioni di una conversazione reale.

1. Prima giornata: da [0,1) a [1,N]

Il docente presenta Math.random(). Restituisce un numero pseudocasuale maggiore o uguale a zero e minore di uno: 0 ≤ Math.random() < 1. In [0,1), la parentesi quadra include lo zero; quella tonda esclude uno. Supponiamo che N sia un intero positivo, piccolo come negli esempi scolastici.

Scena di classe: capire l’intervallo

Docente: Vogliamo un intero tra 1 e N. Che cosa accade se moltiplichiamo il risultato per N?

Studente: L’intervallo [0,1) diventa [0,N).

Docente: Nel modello matematico possiamo ottenere decimali fino a un valore vicinissimo a N, ma mai N.

Studente: Con Math.floor otteniamo 0, 1, 2, …, N−1.

Docente: Che cosa manca per arrivare a 1, 2, …, N?

Studente: Basta aggiungere 1.

function RandomNumero(N) {
    return Math.floor(Math.random() * N) + 1;
}

I passaggi sono: moltiplicare per N, prendere la parte intera inferiore, aggiungere 1. Con N=6 simuliamo un dado. Nel modello uniforme ideale ogni risultato corrisponde a una porzione di ampiezza 1/6 dell’intervallo iniziale. L’implementazione reale è pseudocasuale e approssimativamente uniforme: non è una prova di casualità perfetta.

2. Seconda giornata: un intervallo qualsiasi

Studente: Come generiamo un intero tra a e b, includendo entrambi gli estremi?

Docente: Quanti interi contiene [a,b]?

Studente: Direi b−a.

Docente: Controlliamo [5,10]: 5, 6, 7, 8, 9, 10. Sono sei valori, ma 10−5 vale cinque.

Studente: Allora sono b−a+1, perché contiamo entrambi gli estremi.

Per [5,10] occorrono sei risultati. RandomNumero(10−5+1) produce un intero da 1 a 6. Per ottenere 5, 6, 7, 8, 9, 10 spostiamo tutti i valori di quattro posizioni: aggiungiamo a−1.

3. La risposta dell’intelligenza artificiale

Interrogata direttamente nella nostra simulazione, l’IA propone una formula nota:

function RandomRange(a, b) {
    return Math.floor(Math.random() * (b - a + 1)) + a;
}

Nel modello matematico la spiegazione è corretta: Math.random() produce un valore in [0,1); moltiplicando per b−a+1 arriviamo a [0,b−a+1); Math.floor produce gli interi da 0 a b−a; aggiungendo a otteniamo gli interi da a a b.

Anche gli estremi tornano: con un valore casuale zero otteniamo a; con un valore abbastanza vicino a uno otteniamo b. Le due formule descrivono la stessa trasformazione matematica. Due chiamate separate, però, non devono dare lo stesso numero: effettuano estrazioni diverse.

Il colpo di scena

IA: La funzione richiesta è Math.floor(Math.random() * (b−a+1)) + a.

Studente: La formula è corretta. Abbiamo finito.

Docente: No. Rispetto al progetto che avevamo stabilito, c’è un errore di progettazione.

Studente: Ma i risultati sono corretti. Dov’è l’errore?

Può esistere un difetto importante anche quando il programma restituisce i valori giusti?

4. Dove ha sbagliato l’IA?

Mostra la risposta e la spiegazione

Non è sbagliata la formula. Il problema consiste nell’avere riscritto la generazione casuale ignorando RandomNumero(N), già costruita e verificata. Math.floor(Math.random() * …) ricompare: la conoscenza di basso livello viene duplicata.

RandomRange diventa un secondo punto che conosce Math.random(). La versione coerente con la scelta del progetto delega invece a RandomNumero(b−a+1) e aggiunge a−1.

Docente: L’IA ha risolto il problema ricevuto?

Studente: Sì, la funzione restituisce un numero tra a e b.

Docente: Ha rispettato il progetto già costruito?

Studente: No. Ha risolto nuovamente ciò che RandomNumero sapeva già fare.

Docente: Esatto: nella scena ha affrontato la richiesta locale senza utilizzare la storia e l’architettura del sistema.

Il difetto non appare eseguendo una volta la funzione; appare osservando dipendenze, manutenzione ed evoluzione. Non è un’incapacità inevitabile dell’IA: fornire il contesto e il vincolo di riuso può orientare una risposta diversa. Rimane necessario controllare che il vincolo sia rispettato.

5. Perché la duplicazione diventa pericolosa

Supponiamo di voler registrare ogni estrazione, sostituire la sorgente durante i test o adottare un generatore adatto a requisiti di sicurezza. Modifichiamo la funzione di base:

function RandomNumero(N) {
    const risultato = Math.floor(Math.random() * N) + 1;
    console.log("Numero generato:", risultato);
    return risultato;
}

La versione di RandomRange che usa RandomNumero riceve automaticamente la registrazione. Quella che chiama direttamente Math.random() segue un percorso diverso e non registra nulla. Il messaggio riguarda il valore intermedio da 1 a N: per registrare anche il risultato finale da a a b serve una scelta esplicita al livello superiore.

Conseguenze architetturali

  • Duplicazione: la stessa decisione tecnica compare in più punti.
  • Manutenzione: una modifica deve essere ricordata e ripetuta in tutte le implementazioni.
  • Incoerenza: alcune funzioni possono seguire il nuovo comportamento, altre quello precedente.
  • Testabilità: è più difficile sostituire la sorgente casuale in un unico punto per rendere ripetibili i test.
  • Responsabilità: il livello superiore conosce un dettaglio assegnato al livello di base.

La duplicazione non è soltanto qualche carattere ripetuto. È duplicazione di una decisione. Quando quella decisione cambia, ogni copia diventa un possibile errore. Il vantaggio del riuso vale finché la funzione di base conserva il contratto promesso: un cambiamento incompatibile richiede comunque una revisione dei chiamanti.

6. La lettura secondo il metodo MOSAICO

Nel metodo MOSAICO il software viene costruito per livelli. Le funzioni di base racchiudono capacità elementari e verificate; i livelli superiori le compongono senza riprodurne il funzionamento interno.

  • Livello di base: RandomNumero(N) produce un intero casuale da 1 a N.
  • Livello superiore: RandomRange(a,b) calcola quanti valori contiene [a,b] e trasla il risultato.
  • Confine: nel nostro esempio soltanto RandomNumero conosce Math.random e Math.floor.
  • Vantaggio: le modifiche interne compatibili con il contratto si propagano ai chiamanti.

La formula diretta attraversa questo confine. La soluzione MOSAICO rende invece visibile la gerarchia delle idee: RandomRange → RandomNumero → Math.random.

Una domanda è: «Qual è la formula più diretta per ottenere un intero tra a e b?». La domanda del progettista è: «Quale capacità possiede già il sistema e come posso comporla per ottenere il nuovo comportamento?». È questa seconda domanda a trasformare una collezione di funzioni in un’architettura.

7. Una precisazione importante

In un programma isolato, nel quale RandomNumero non esiste, la formula diretta è appropriata agli esempi proposti. Non esiste un divieto universale di chiamare Math.random(). L’errore nasce dal contesto: avevamo deliberatamente costruito un livello di base e volevamo riutilizzarlo.

Il buon progetto non consiste nell’aggiungere funzioni intermedie ovunque. Consiste nell’attribuire chiaramente una responsabilità e nel non duplicarla quando è già stata centralizzata.

Possiamo controllare che a e b siano interi e che a≤b:

function RandomRange(a, b) {
    if (!Number.isInteger(a) || !Number.isInteger(b)) {
        throw new Error("a e b devono essere interi");
    }
    if (a > b) {
        throw new Error("a non può essere maggiore di b");
    }
    return RandomNumero(b - a + 1) + a - 1;
}

La relazione fra a e b appartiene alla funzione di intervallo. La funzione di base deve invece garantire il proprio contratto su N, anche quando viene chiamata direttamente. Questa versione è didattica, non una libreria generale per tutti i valori Number.

Nota tecnica: limiti e sicurezza

Number.isInteger non garantisce che un numero sia un intero sicuro. Per una libreria reale occorre definire il dominio ammesso, controllare anche l’ampiezza b−a+1 e considerare gli arrotondamenti in virgola mobile. Perfino la sola verifica degli interi sicuri non dimostra una distribuzione esattamente uniforme su intervalli enormi. Gli esempi qui usano piccoli intervalli come [1,6], [5,10] e [−3,3].

Math.random non è un generatore crittograficamente sicuro: questi esempi non servono a generare password, token o segreti. Centralizzare il generatore facilita una sostituzione, ma non rende automaticamente sicura la trasformazione dei suoi risultati.

Conclusione: la nuova competenza del programmatore

Una richiesta locale può ricevere una risposta localmente corretta. Il sistema complessivo possiede però una storia: funzioni disponibili, responsabilità assegnate, dipendenze consentite e decisioni da mantenere centralizzate.

Il programmatore deve chiedersi se la soluzione appartenga al progetto. Occorrono memoria architetturale, conoscenza delle conseguenze e volontà di non accettare automaticamente il codice soltanto perché funziona.

La differenza è fra «So calcolare direttamente il risultato» e «Il sistema sa già fare la parte fondamentale: devo riutilizzarla e aggiungere soltanto ciò che manca».

Il difetto non era nel numero restituito. Era nella decisione di ignorare una conoscenza già organizzata. Il passaggio da RandomNumero(N) a RandomRange(a,b) è piccolo, ma rappresenta bene la filosofia MOSAICO: riutilizzare non soltanto codice, ma responsabilità e decisioni.

Riferimenti tecnici

Leggi anche

Il programmatore nell’epoca dell’intelligenza artificiale