Da Junior a Senior con ChatGPT
Da programmatori junior a senior con ChatGPT
Cosa siano l'AI e più in generale i Large Language Model è ormai noto a tutti. Ognuno di noi ha scritto innumerevoli prompt per ottenere risposte sensate, con risultati più o meno soddisfacenti. Per comprendere al meglio come questa gamma di prodotti possa aiutarci a diventare programmatori migliori, passando da junior a senior, o semplicemente dei tecnici più esperti, dobbiamo prima capire cosa sia l'ingegneria dei prompt, perché sia così importante e quali siano gli ambiti in cui un LLM come ChatGPT o Claude o altri possano aiutarci a crescere professionalmente.
Esperienza sul campo
Quanto sto per scrivere nasce dall'esperienza di mesi di utilizzo di ChatGPT, GitHub Copilot, Claude, Gemini e altri LLM, nonché da una serie di letture e studi sull'argomento. Sono consapevole che fra qualche mese potrei essere smentito: potrebbero emergere, o forse sono già emersi e non li utilizzo ancora, strumenti migliori che aiutano in modo ancora più significativo gli sviluppatori (e non solo) a crescere professionalmente. Sicuramente alcuni di voi utilizzano strumenti diversi da quelli che uso io, ma proviamo a concentrarci su ciò che possiamo far fare a prodotti di questo genere e non su quale sia il migliore, altrimenti si innescherebbe una guerra infinita dalla quale, ne sono certo, nessuno uscirebbe vincitore.
Comprendere ChatGPT e, in modo più ampio, i LLM
Partiamo dal principio: ChatGPT è un chatbot di intelligenza artificiale (AI) che utilizza l'elaborazione del linguaggio naturale per creare un dialogo simile a quello umano. Il modello linguistico può rispondere a domande e comporre vari contenuti scritti, tra cui articoli, post sui social media, saggi, codice ed e-mail, rendendolo uno strumento entusiasmante per programmatori di tutti i livelli. Si tratta quindi di un modello che non ragiona, ma è in grado di applicare in maniera molto efficace le regole che gli sono state insegnate durante il training. Ciò significa che se gli diamo un input sbagliato, otterremo un output sbagliato, ma se diamo un input corretto, otterremo un output corretto. Allo stesso modo, se utilizziamo un LLM che ha utilizzato dataset con errori, di conseguenza avremo un output con errori. Per loro natura, i modelli tentano sempre di dare delle risposte e, di conseguenza, esiste una percentuale di risposte, più o meno grande, che sarà sempre sbagliata, inventata o comunque non corretta.
Fissato questo punto iniziale, il cui scopo è quello di focalizzare il tipo di strumento che stiamo tentando di utilizzare, proviamo a capire quali sono gli ambiti in cui la soglia d'errore è più alta e quali invece quelli in cui la soglia d'errore si abbassa e possiamo utilizzarlo con maggiore successo.
Gli LLM non sono pensati per risolvere problemi o indovinelli
Gli LLM sono stati creati per rispondere a domande, scrivere articoli, generare codice, ma non sono stati progettati per pensare e risolvere problemi o indovinelli. Ciò significa che se poniamo loro un problema, non sapranno risolverlo autonomamente; tuttavia, se forniamo un problema già risolto, sapranno restituirci la soluzione.
Ogni volta che vedo un video in cui qualcuno si diverte a proporre un indovinello a ChatGPT e poi si stupisce che non riesca a risolverlo, non posso fare a meno di sorridere. È come chiedere a un bambino di 5 anni di risolvere un problema di fisica quantistica: sono test che non tengono conto delle capacità dell'interlocutore e del modo in cui è stato concepito. Non aggiungono alcun valore e non dimostrano che il modello sia inadeguato, ma piuttosto evidenziano che il "creator" di turno non ha compreso come utilizzare il modello correttamente.
Nel campo della programmazione risolvere un indovinello conta poco, quello che serve e velocizzare i processi di produzione del codice, migliorare la qualità del codice e la comprensione dello stesso. In questi ambiti, gli LLM possono dare un contributo significativo, ma solo se utilizzati correttamente.
La scrittura dei prompt
Si parla spesso di come scrivere i prompt per ottenere il miglior risultato, ma non sempre si capisce cosa significhi. Scrivere un prompt significa fornire indicazioni chiare e precise su cosa ci aspettiamo dal modello e cosa vogliamo ottenere.
Le prime e più ovvie indicazioni sono:
- Sii chiaro e conciso
- Sii specifico
- Includi il contesto e i dettagli necessari
- Definisci input e output
Se applichiamo queste indicazioni alla programmazione, ci rendiamo conto che non è così semplice come sembra. Definire un prodotto software in modo chiaro, conciso e specifico richiede esperienza e conoscenza delle best practice. Più il progetto è complesso, più è difficile definirlo in questo modo.
Quante volte vi siete trovati davanti a specifiche poco chiare o variate nel tempo, senza i dettagli necessari? Quante volte avete scritto codice senza sapere esattamente cosa si aspettava il cliente?
In questi casi, ottenere un risultato corretto da un LLM diventa difficile, anche con un prompt chiaro e dettagliato.
Quali prompt scrivere?
Abbiamo capito che un prompt troppo grande è irrealistico, mentre uno troppo piccolo non porta al risultato desiderato. Cosa fare, allora?
La soluzione è scrivere prompt il più possibile specifici, includendo il contesto e i dettagli necessari, ma mantenendo chiarezza e concisione.
Un esempio potrebbe essere:
Scrivi una funzione C# per trovare il fattoriale di un numero dato.
Qui abbiamo specificato il linguaggio di programmazione, la funzionalità richiesta e il contesto necessario, ottenendo un risultato efficace e preciso.
Ricordiamoci che i modelli apprendono dai contesti già affrontati. Maggiore è la comunanza del contesto, maggiore è la probabilità di ottenere un risultato soddisfacente.
Al contrario, più il contesto è specifico, maggiore è la possibilità che il modello non sia in grado di fornire una risposta corretta, anche con tutti i dati necessari e una descrizione chiara.
In questi casi, è preferibile usare un prodotto come Copilot o Codeium, in grado di analizzare il codice e fornire risposte più pertinenti al contesto e alle necessità.
Limiti di conoscenza
Un altro punto importante da tenere in considerazione è che gli LLM non possiedono una conoscenza infinita, bensì limitata a ciò che è stato inserito nei dataset di training. Questo implica che se forniamo un input non presente nei dataset, il modello non sarà in grado di rispondere adeguatamente.
Quando si verifica più frequentemente questa situazione? Quando proviamo a chiedere a un LLM informazioni su prodotti che non esistevano o erano appena stati lanciati nel momento in cui il modello ha iniziato ad essere addestrato. Cominciano ad emergere delle mitigazioni a questo aspetto, come l'interrogazione in tempo reale della rete e il training continuo, ma sono tutte soluzioni che non risolvono completamente il problema. Al momento lo attenuano, riuscendo in parte a fornire delle risposte, ma in misura ancora maggiore a inventarsele. Non aspettatevi quindi di ottenere risposte accurate da ChatGPT su prodotti di recente uscita, tecnologie appena rilasciate o eventi accaduti da poco, perché non saprà rispondervi correttamente o, se lo farà, sarà con risposte inventate o di bassa qualità.
No, non basta interrogare la rete in tempo reale per ottenere risposte corrette, perché il modello non ha la capacità di capire se le informazioni che sta leggendo sono corrette o meno e normalmente, ogni tecnologia che esce, ha usa serie di problemi documentativi e di comprensione, oltre alla scarsità di applicazioni disponibili in Internet.
Come possiamo avere dei risultati migliori?
Detto questo è facile comprendere che, per avere i risultati migliori, è meglio che la tecnologia sulla quale stiamo lavorando sia ben consolidata e che lo strumento al quale ci appoggiamo possa contestualizzare al massimo le nostre domande.
Un modo per sfruttare al meglio gli LLM è quindi quello di affidarsi a modelli che non si limitano al solo prompt digitato, ma possano aggiungere una serie di dati presi dal progetto sul quale stiamo lavorando. Questa gamma di prodotti sono in grado di dare risposte più accurate perché hanno una conoscenza più ampia e dettagliata rispetto a quella che può avere un modello basato solo su prompt.
Un esempio di questi modelli è GitHub Copilot, che è in grado di fornire risposte migliori rispetto a ChatGPT, grazie a una conoscenza più ampia e dettagliata.
La conoscenza più ampia deriva dal fatto che, per generare un suggerimento, viene analizzato il codice prima e dopo la posizione del cursore all'interno dell'editor, vengono utilizzati tutti i file aperti in quel momento e vengono aggiunti i repository GitHub referenziati nel contesto. Tutti questi dati fanno parte dell'input inviato a Copilot e, di conseguenza, il codice suggerito non è un semplice copia e incolla di qualcosa trovato nei dati di allenamento, ma è codice fortemente contestualizzato rispetto all'ambito in cui viene utilizzato.
Facciamo un piccolo esempio, partendo da questo codice Java, che potrebbe però essere scritto in qualsiasi linguaggio di programmazione:
public static final int UNO = 1;
public static final int DUE = 2;
public static final int TRE = 3;
public static final int QUATTRO = 4;
public static final int CINQUE = 5;
public static void main(String[] args) {
// Dato il parametro cerco la corrispondenza rispetto alle statiche
}
Soffermiamoci sul commento, che è molto generico e va contro tutte le regole di prompt engineering che suggeriscono delle forti tipizzazioni. Se fossimo, ad esempio, in ChatGPT, mancherebbe l'indicazione del linguaggio da usare e si potrebbe avere in risposta una dato poco significativo:
In questo contesto, invece, Copilot è in grado di capire che il commento è riferito alle variabili statiche, che si parla di Java e, di conseguenza, ci suggerirà un codice che fa esattamente quello che ci aspettiamo.
int parametro = Integer.parseInt(args[0]);
switch (parametro) {
case UNO:
System.out.println("UNO");
break;
case DUE:
System.out.println("DUE");
break;
case TRE:
System.out.println("TRE");
break;
case QUATTRO:
System.out.println("QUATTRO");
break;
case CINQUE:
System.out.println("CINQUE");
break;
default:
System.out.println("Nessuna corrispondenza");
}
Capisco che a questo punto moltissimi di voi si staranno dicendo: "il codice è troppo semplice", "si poteva fare di meglio" e avete ragione, ma il punto non è quello. Il punto è che gli strumenti contestualizzati come Copilot sono in grado di capire meglio i nostri prompt o quello di cui abbiamo bisogno e di dare una risposta corretta, mentre ChatGPT non sarebbe in grado di farlo, a meno di non scrivere un prompt molto più esteso di quel commento minimale inserito all'interno del codice.
Contestualizzare la domanda o l'aiuto che si vuole ottenere dà un enorme vantaggio rispetto a scrivere un prompt generico, perché permette al modello di capire meglio cosa ci si aspetta da lui e di dare una risposta migliore.
Comprensione del codice
Spesso ci troviamo di fronte a codice che non abbiamo scritto noi e che non conosciamo, ma che dobbiamo modificare o estendere. In questi casi è molto utile avere un modello che ci aiuti a capire cosa fa il codice e come funziona.
Un programmatore senior normalmente non ha grossi problemi nella comprensione del codice, ma abbassando il livello a junior o semplicemente uscendo dalla comfort zone del senior, ci si rende conto che non è così scontato capire cosa fa un pezzo di codice scritto da qualcun altro.
Parlando di esperienza personale potete chiedermi di descrivere qualsiasi programma con una sintassi derivata dal C, ma non chiedetemi di descrivere un programma scritto in Perl, perché probabilmente fingerei un attacco di letargia.
I modelli LLM ci possono aiutare in questa fase, perché sono in grado di analizzare il codice e di darci una spiegazione di cosa fa e come funziona. Questo ci permette di capire meglio il codice e di riuscire a modificarlo o estenderlo in modo più efficace.
Anche in questo caso la contestualizzazione è molto importante, ma proviamo comunque ad affrontare il problema con un prompt generico e vediamo cosa succede.
Partiamo da questa funzione presa dal repository https://github.com/jetty/jetty.project
public static boolean matchesURI(URI uri1, URI uri2)
{
String scheme = uri1.getScheme();
if (scheme.equalsIgnoreCase(uri2.getScheme()))
{
if (uri1.getHost().equalsIgnoreCase(uri2.getHost()))
{
// Handle default HTTP ports.
int thisPort = HttpClient.normalizePort(scheme, uri1.getPort());
int thatPort = HttpClient.normalizePort(scheme, uri2.getPort());
if (thisPort == thatPort)
{
// Use decoded URI paths.
return uri2.getPath().startsWith(uri1.getPath());
}
}
}
return false;
}
Proviamo ora a chiedere a Claude (ma sarebbe la stessa cosa con altri LLM) di spiegarci cosa fa questa funzione.
mi descrivi questo codice
<codice>
In risposta avremo
Questo codice definisce un metodo statico chiamato `matchesURI` che confronta due oggetti URI (Uniform Resource Identifier) per determinare se corrispondono secondo criteri specifici. Ecco una descrizione dettagliata del funzionamento:
1. Il metodo prende due parametri: `uri1` e `uri2`, entrambi di tipo URI.
2. Confronta prima gli schemi (protocolli) dei due URI:
- Estrae lo schema di `uri1`
- Confronta gli schemi di `uri1` e `uri2` ignorando le maiuscole/minuscole
3. Se gli schemi corrispondono, procede a confrontare gli host:
- Confronta gli host di `uri1` e `uri2` ignorando le maiuscole/minuscole
4. Se anche gli host corrispondono, passa al confronto delle porte:
- Utilizza un metodo `HttpClient.normalizePort()` per gestire le porte predefinite HTTP
- Confronta le porte normalizzate di entrambi gli URI
5. Se le porte corrispondono, controlla infine i percorsi:
- Verifica se il percorso decodificato di `uri2` inizia con il percorso decodificato di `uri1`
6. Il metodo restituisce `true` se tutte queste condizioni sono soddisfatte, altrimenti restituisce `false`.
In sintesi, questo metodo considera due URI corrispondenti se hanno lo stesso schema, host, porta (considerando le porte HTTP predefinite) e se il percorso di `uri2` inizia con il percorso di `uri1`.
Capite che, in tutti i contesti dove ci troviamo di fronte a codice che non conosciamo, un LLM può essere molto utile per capire cosa fa e come funziona.
Creazione dei test
Ci troviamo spesso di fronte alla scrittura di test per il codice che abbiamo scritto o che dobbiamo scrivere, dipende un po' dalla scuola di pensiero che avete adottato o dal tempo che vi viene dato per raggiungere uno scopo. Scrivere test però non è sempre facile e richiede una certa esperienza e conoscenza delle best practice.
Un LLM può esserci d'aiuto, perché è in grado di comprendere porzioni di codice e di generare dei test per quel codice, oppure di immaginarne alcuni per il codice che dovremo andare a scrivere. Questo ci permette di avere velocemente dei test pronti e di concentrarci solo sui casi non coperti o sui casi limite.
Chiaramente potremmo trovarci di fronte anche dei test parziali: ma è molto più veloce integrare una suite di test che scriverla da zero.
Partendo sempre dall'esempio di cui sopra possiamo chiedere:
mi scrivi i test per questa funzione
In questo caso ho utilizzato Claude, che in molti casi preferisco a ChatGTP, e come risultato ho ottenuto:
Certamente, posso aiutarti a scrivere alcuni test per questa funzione.
Userò JUnit, un framework di test ampiamente utilizzato per Java, per creare questi test. Ecco una serie di test che coprono diversi scenari:
Questi test coprono diversi scenari per la funzione matchesURI:
- URI corrispondenti
- Schemi diversi
- Host diversi
- Corrispondenza con porta predefinita
- Porte diverse
- Percorsi non corrispondenti
- Insensibilità alle maiuscole/minuscole per schema e host
- URI con query e frammento
Questi test dovrebbero fornire una buona copertura per la funzione matchesURI. Tuttavia, potresti voler aggiungere ulteriori test per casi limite o scenari specifici del tuo uso della funzione.
Di seguito riporto il pezzo di codice creato che rappresenta un'ottima base di partenza:
import org.junit.Test;
import static org.junit.Assert.*;
import java.net.URI;
public class URIMatcherTest {
@Test
public void testMatchingURIs() throws Exception {
URI uri1 = new URI("http://example.com/path");
URI uri2 = new URI("http://example.com/path/subpath");
assertTrue(URIMatcher.matchesURI(uri1, uri2));
}
@Test
public void testNonMatchingScheme() throws Exception {
URI uri1 = new URI("http://example.com/path");
URI uri2 = new URI("https://example.com/path");
assertFalse(URIMatcher.matchesURI(uri1, uri2));
}
@Test
public void testNonMatchingHost() throws Exception {
URI uri1 = new URI("http://example.com/path");
URI uri2 = new URI("http://example.org/path");
assertFalse(URIMatcher.matchesURI(uri1, uri2));
}
@Test
public void testMatchingWithDefaultPort() throws Exception {
URI uri1 = new URI("http://example.com/path");
URI uri2 = new URI("http://example.com:80/path/subpath");
assertTrue(URIMatcher.matchesURI(uri1, uri2));
}
@Test
public void testNonMatchingPort() throws Exception {
URI uri1 = new URI("http://example.com:8080/path");
URI uri2 = new URI("http://example.com:9090/path");
assertFalse(URIMatcher.matchesURI(uri1, uri2));
}
@Test
public void testNonMatchingPath() throws Exception {
URI uri1 = new URI("http://example.com/path1");
URI uri2 = new URI("http://example.com/path2");
assertFalse(URIMatcher.matchesURI(uri1, uri2));
}
@Test
public void testCaseInsensitiveSchemeAndHost() throws Exception {
URI uri1 = new URI("HTTP://EXAMPLE.COM/path");
URI uri2 = new URI("http://example.com/path/subpath");
assertTrue(URIMatcher.matchesURI(uri1, uri2));
}
@Test
public void testWithQueryAndFragment() throws Exception {
URI uri1 = new URI("http://example.com/path");
URI uri2 = new URI("http://example.com/path/subpath?query=value#fragment");
assertTrue(URIMatcher.matchesURI(uri1, uri2));
}
}
Come vedete ci sono molte idee e parecchi test funzionanti che possiamo integrare senza timore nella nostra struttura di progetto. Più manteniamo il nostro approccio al codice di tipo KISS (Keep It Simple Stupid) più possiamo ricevere dei buoni test da parte del modello, limitando le casistiche d'errore.
Sicurezza
Un altro ambito in cui un LLM può essere molto utile è la sicurezza. Scrivere codice sicuro non è sempre facile e richiede una certa esperienza e conoscenza delle best practice.
Esistono tool che aiutano a scrivere codice migliore e nella mia vita credo di aver guardato i log di SonarQube più del viso di mia moglie, ma un modello LLM può essere un valido aiuto anche in questo ambito, più che altro perch* la sua conoscenza a livello di linguaggi di programmazione è decisamente superiore a molti altri tool.
Per esempio, sempre partendo dal metodo che stiamo analizzando, ho provato a chiedere:
Mi dai dei suggerimenti per rendere il codice più sicuro?
E ho ottenuto qualcosa che mi ero già chiesto mentre guardavo il metodo:
Gestione dei null:
Il metodo attuale non controlla se gli URI in input sono null. Aggiungi controlli all'inizio del metodo:
if (uri1 == null || uri2 == null) {
throw new IllegalArgumentException("URIs cannot be null");
}
In questo caso ho tagliato gli altri suggerimenti: è però importante notare che il modello ha centrato un punto di miglioramento dato dalla normalizzazione dei parametri.
Refactoring
Il refactoring è un ambito molto importante della programmazione. Per poter fare un refactoring olistico occorre avere una visione che globale del progetto che al momento non è ancora nelle corde dei modelli LLM. Se però ci concentriamo sul concetto di KISS, riducendo in modo sapiente l'ambito di azione di un LLM, allora possiamo avere dei vantaggi non indifferenti o comunque una serie di idee da percorrere.
Fra le operazioni di refactoring sono presenti anche le operazioni di ottimizzazione del codice, che possono essere utili per rendere più efficienti i nostri programmi.
L'8 giugno 2023 su Nature usciva un articolo dal tema ottimizzazione di codice e algoritmi di sort: https://www.nature.com/articles/s41586-023-06004-9
Si analizzava come, utilizzando un deep reinforcement learning, si riusciti ad ottimizzare il codice assembler utilizzato per un algoritmo di sort.
Senza arrivare questo tipo di risultati, possiamo comunque chiedere a un LLM di ottimizzare il nostro codice, partendo da un prompt generico come:
Puoi fare un refactoring per velocizzare questo codice?
Il test è sempre fatto sul codice di Jetty ed il risultato è stato questo:
public static boolean matchesURI(URI uri1, URI uri2) {
// Memorizza lo schema, l'host e la porta una sola volta.
String scheme1 = uri1.getScheme();
String scheme2 = uri2.getScheme();
if (scheme1 == null || !scheme1.equalsIgnoreCase(scheme2)) {
return false;
}
String host1 = uri1.getHost();
String host2 = uri2.getHost();
if (host1 == null || !host1.equalsIgnoreCase(host2)) {
return false;
}
// Porta normalizzata una volta.
int port1 = HttpClient.normalizePort(scheme1, uri1.getPort());
int port2 = HttpClient.normalizePort(scheme2, uri2.getPort());
if (port1 != port2) {
return false;
}
// Confronta il percorso alla fine.
String path1 = uri1.getPath();
String path2 = uri2.getPath();
return path2.startsWith(path1);
}
Probabilmente non abbiamo guadagnato molto in termini di prestazioni, ma dal punto di vista della leggibilità il metodo sembra più chiaro e lineare, evitando un codice annidato su più livelli.
Uno degli scopi del refactoring è anche quello di rendere più comprensibile il codice, anche perché dietro a centinaia di righe di codice ci sono sempre delle persone che all'aumentare della complessità, diminuiranno la voglia o l'efficacia delle proprie modifiche. Mantenere del codice semplice facilita la manutenzione ed evita di leggere commenti come questi che sono ormai diventati dei meme:
// This code is a total mess and I have no idea what it does, but it works, so don't touch it!
Boilerplate
Quanti di noi si sono trovati a dover scrivere un nuovo progetto e sono partiti da un boilerplate? Credo tutti, e ogni volta il passo iniziale richiede tempo e attenzione per evitare errori o dimenticanze.
Per questo motivo esistono raccolte di boilerplate che offrono un punto di partenza. Un esempio è questo progetto GitHub: https://github.com/melvin0008/awesome-projects-boilerplates, che raccoglie molti esempi di progetti minimali.
Anche qui un LLM può aiutarci a scrivere il boilerplate, partendo da un prompt generico come:
Puoi scrivermi un progetto minimale VUE composto da una prima maschera di login, che interroga il servizio https://reqres.in/api/login passando un JSON con i parametri "email" e "password", e in caso di risposta con codice 200 mi permette di accedere a un backend composto da un pannello a sinistra con un menu e un pannello centrale dove visualizzare dei dati. Nel pannello di sinistra deve esserci il tasto di logout.
Evito di riportare qui tutta la risposta: ho incluso il risultato in un progetto GitHub https://github.com/matteobaccan/VUEdashboard, del quale vi consiglio di guardare i primi commit, in quanto, nel momento in cui leggerete, potrebbe essere stato esteso con ulteriori modifiche.
Quanto ho ottenuto è basilare, ma offre un punto di partenza per i junior che devono impostare un progetto e non hanno idea di come farlo e velocizza il lavoro per i senior, che invece hanno tutte le basi per farlo, ma vogliono concentrarsi su altro.
Nel mio caso ho preso quello che ChatGPT ha prodotto, ho creato un nuovo progetto VUE 3, ho corretto alcuni problemi di nomenclatura, ho aggiornato le dipendenze e ho unito il tutto in un progetto funzionante.
Sì, lo so, la cosa migliore è sempre studiare e saper fare queste cose da zero e con una mano legata dietro la schiena, ma molto spesso una spinta in una direzione ci permette di essere più veloci e magari di scoprire qualcosa che è all'esterno della nostra comfort zone dalla quale non usciamo mai.
Pensate solo a quante volte ci siamo bloccati nella scrittura di qualcosa e un boilerplate ci avrebbe velocizzato, ma non se ne trovava mai uno che faceva esattamente al caso nostro? Bene, un LLM colma la differenza fra quello che vorremmo e quello che troviamo, diminuendone la distanza.
Quanto è fastidioso commentare il codice
Alzi la mano chi non ha mai scritto codice senza commentarlo, o chi ha dovuto commentare codice scritto da altri senza capirlo.
Anche in questo caso, un LLM può aiutarci a commentare il codice, partendo da un prompt generico come:
Aggiungi dei commenti al seguente codice
private String readFile(String fileName){
BufferedReader br;
try {
br = new BufferedReader(new FileReader(fileName));
final StringBuilder sb = new StringBuilder();
String line = br.readLine();
while (line != null) {
sb.append(line);
sb.append(System.lineSeparator());
line = br.readLine();
}
br.close();
return sb.toString();
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
Come risultato, otterremo:
/**
* Legge il contenuto di un file e lo restituisce come stringa.
* @param fileName Il nome del file da leggere
* @return Il contenuto del file come stringa, o una stringa vuota in caso di errore
*/
private String readFile(String fileName) {
BufferedReader br;
try {
// Inizializza un BufferedReader per leggere il file
br = new BufferedReader(new FileReader(fileName));
// Crea un StringBuilder per costruire il contenuto del file
final StringBuilder sb = new StringBuilder();
// Legge la prima linea del file
String line = br.readLine();
// Continua a leggere finché ci sono linee nel file
while (line != null) {
// Aggiunge la linea corrente al StringBuilder
sb.append(line);
// Aggiunge un separatore di linea del sistema
sb.append(System.lineSeparator());
// Legge la prossima linea
line = br.readLine();
}
// Chiude il BufferedReader
br.close();
// Restituisce il contenuto del file come stringa
return sb.toString();
} catch (IOException e) {
// Gestisce eventuali errori di I/O
e.printStackTrace();
}
// Restituisce una stringa vuota in caso di errore
return "";
}
In questo modo, il codice risulta commentato e molto più comprensibile. Anche se l'esempio è semplice, il concetto di fondo è chiaro: avete un software sconosciuto da commentare? Chiedete a un LLM di farlo e potreste trasformare la vostra codebase da un inferno a un paradiso.
Ecco una versione revisionata del testo:
Revisione del Codice nelle Pull Request
Immaginate di dover gestire le Pull Request (PR) di un repository e di voler ottimizzare il lavoro. Su GitHub esistono diverse action che possiamo implementare per eseguire delle code review automatiche. Una di queste è:
https://github.com/magnificode-ltd/chatgpt-code-reviewer
Questa action consente di eseguire una revisione automatica del codice nelle PR, identificando in modo preciso le modifiche e suggerendo miglioramenti. È solo un esempio, ma la cosa interessante è che possiamo creare le nostre action personalizzate, da attivare sulle PR, che ci aiutino a velocizzare il lavoro e migliorare la qualità del codice.
Analisi del Progetto
Da senior mi trovo spesso ad analizzare un progetto o a leggerne la documentazione fatta da altri ed è sempre un'operazione complessa che richiede tempo e attenzione. In passato affrontavo questa fase studiando e facendo ricerche su Internet nella speranza di trovare informazioni utili.
Queste ricerche spesso generavano molte idee, ma risultavano dispersive: nonostante la bravura nella ricerca, è facile perdersi in un mare di informazioni e non trovare ciò che serve.
Di recente, ho cambiato approccio: dopo una prima fase di studio per inquadrare il progetto, mi perdo in alcune conversazione vocale con ChatGPT. Questo approccio ha una serie di interessanti vantaggi:
- È molto più rapido rispetto alla ricerca su internet: le risposte sono mirate e contestuali.
- Si può fare brainstorming con il modello: rispetto alla ricerca tradizionale, la conversazione permette collegamenti trasversali tra le informazioni senza dover aprire nuovi tab e iniziare nuove ricerche.
- Funziona anche in auto: durante i lunghi viaggi verso i clienti, parlare con un assistente virtuale è molto meglio che rimanere in silenzio (no, non sono un amante dei silent travel)
- Fornisce informazioni continue che, anche se non immediatamente utilizzabili, possono stimolare nuove idee.
- Parlando ad alta voce è più facile da memorizzare rispetto alla semplice lettura.
- Si ci potrebbero essere delle allucinazioni, ma valutando quanti errori si leggono all'interno dei post presenti in rete, non è detto che questo sia un problema.
Naturalmente, chi mi vede fermo al semaforo a parlare da solo potrebbe pensare che sono pazzo, ma nella mia testa mi sento come Michael Knight che parla con KITT.
Traduzioni
Uno degli aspetti più tediosi per un programmatore è "tradurre" la documentazione di un progetto in un'altra lingua. È un lavoro noioso che richiede tempo e attenzione, ma è necessario per rendere il progetto comprensibile a chi non parla la nostra lingua.
Chiaramente l'approccio migliore sarebbe scrivere tutto in inglese, ma non è detto che questa sia sempre la soluzione ottimale.
Abbiamo la documentazione di progetto in spagnolo scritta dalla filiale di Madrid e dobbiamo tradurla in inglese per farla leggere ai colleghi di New York.
Da qualche anno, ho adottato il markdown per tutta la documentazione che produco. È un formato di scrittura leggero che permette di creare documenti in modo semplice e rapido, senza preoccuparsi troppo della formattazione e senza avere il vincolo di installare necessariamente qualcosa sul proprio computer.
Questo mi consente di realizzare velocemente slide con strumenti come Marp e di mantenere facilmente il controllo di versione.
Recentemente, mi sono trovato a dover tradurre la documentazione di un progetto in inglese. Farlo manualmente, copiando e incollando il testo in un LLM e poi nel file, è noioso e dispendioso in termini di tempo. Ero sul punto di scrivere un programma per automatizzare il processo, quando ho scoperto l'esistenza di estensioni per GitHub che lo fanno automaticamente, come GPT-Translate.
https://github.com/3ru/gpt-translate
Sono certo che esistano altre soluzioni, ma questa si è integrata perfettamente nel ciclo di vita dei progetti che gestisco, velocizzando notevolmente il lavoro di traduzione.
Attraverso una semplice issue, è possibile attivare l'action di GPT-Translate, specificare quale documento markdown tradurre e in quale lingua. In risposta, viene prodotta una PR con il documento tradotto, pronta per essere approvata.
Ti svelerò un segreto: se stai leggendo questo documento in inglese o spagnolo, fino ad ora hai letto le traduzioni fatte da GPT-Translate tramite ChatGPT.
Formattazioni
Vi è mai capitato di dover affrontare codice o strutture dati non formattate correttamente? I modelli LLM possono aiutarci anche in questo.
Ad esempio, partendo da un prompt come:
Mi formatti questa struttura JSON?
[{"field1":"aa11","field2":"aa22", "structField":{"sf1" : "aaa11","sf2" : "aaa22"}},
{"field1":"bb11","field2":"bb22","structField":{"sf1" : "bbb11","sf2" : "bbb22"}}]
Otterremo:
[
{
"field1": "aa11",
"field2": "aa22",
"structField": {
"sf1": "aaa11",
"sf2": "aaa22"
}
},
{
"field1": "bb11",
"field2": "bb22",
"structField": {
"sf1": "bbb11",
"sf2": "bbb22"
}
}
]
E se ci serve in un altro formato, possiamo chiedere:
Mi converti questo JSON in formato XML?
Il risultato sarà:
<items>
<item>
<field1>aa11</field1>
<field2>aa22</field2>
<structField>
<sf1>aaa11</sf1>
<sf2>aaa22</sf2>
</structField>
</item>
<item>
<field1>bb11</field1>
<field2>bb22</field2>
<structField>
<sf1>bbb11</sf1>
<sf2>bbb22</sf2>
</structField>
</item>
</items>
In questo caso, ho usato ChatGPT, ma la logica è la stessa per tutti i modelli LLM: sono in grado di formattare le strutture dati. Anche se queste funzionalità sono già presenti negli IDE moderni, avere un modello che lo fa per noi è sicuramente un vantaggio.
Conversione di Codice
Quante volte vi siete trovati a dover convertire del codice da un linguaggio di programmazione all'altro? Un esempio comune è la necessità di utilizzare librerie o API fornite solo in un determinato linguaggio, con scarsa o nessuna documentazione standardizzata.
Per alcuni progetti dove mi sono spaccato la testa in passato, ho passato ore del mio tempo a convertire chiamate C, alimentare a strutture, in chiamate Java con librerie di conversione come JNA.
Se avessi avuto allora a disposizione un LLM avrei potuto fare:
Mi converti questo codice da Java a JavaScript?
public static void PrimeNumberCheck(String[] args) {
int numberToCheck = 17;
boolean isPrime = true;
for (int i = 2; i <= numberToCheck / 2; i++) {
int remainder = numberToCheck % i;
System.out.println(numberToCheck + " divided by " + i + " gives a remainder " + remainder);
if (remainder == 0) {
isPrime = false;
break;
}
}
if (isPrime) {
System.out.println(numberToCheck + " is a prime number");
} else {
System.out.println(numberToCheck + " is not a prime number");
}
}
Con un grande risparmio di tempo:
function PrimeNumberCheck(args) {
let numberToCheck = 17;
let isPrime = true;
for (let i = 2; i <= numberToCheck / 2; i++) {
let remainder = numberToCheck % i;
console.log(numberToCheck + " divided by " + i + " gives a remainder " + remainder);
if (remainder == 0) {
isPrime = false;
break;
}
}
if (isPrime) {
console.log(numberToCheck + " is a prime number");
} else {
console.log(numberToCheck + " is not a prime number");
}
}
La cosa interessante è la matrice di conversione possibile in questi grandi modelli che è generalmente molto più ampia di quanto possibile fino a pochi mesi fa.
Generazione di Dati Casuali
Uno dei compiti più fastidiosi per un programmatore è generare dati di test per il proprio codice. Anche in questo caso, i modelli LLM possono venirci in aiuto.
Puoi generare un array JSON con 10 elementi contenenti i campi nome e cognome con dati casuali?
ed ecco pronti i dati che mi servivano:
[
{
"nome": "Giovanni",
"cognome": "Rossi"
},
{
"nome": "Maria",
"cognome": "Bianchi"
},
{
"nome": "Luca",
"cognome": "Verdi"
},
{
"nome": "Paola",
"cognome": "Neri"
},
{
"nome": "Marco",
"cognome": "Gialli"
},
{
"nome": "Sara",
"cognome": "Blu"
},
{
"nome": "Andrea",
"cognome": "Rosa"
},
{
"nome": "Giulia",
"cognome": "Viola"
},
{
"nome": "Alessandro",
"cognome": "Arancione"
},
{
"nome": "Francesca",
"cognome": "Marrone"
}
]
Avrei potuto chiedere dati strutturati in maniera molto più articolata o formati differenti: XML, CSV e coì via, e il modello mi avrebbe aiutato a generarli. Questo è un aspetto molto interessante, perché spesso la generazione di dati di test è un compito noioso e ripetitivo, che può essere automatizzato con l'aiuto di un modello LLM.
Regular Expression
Ci sono due scuole di pensiero sulle regular expression: chi le odia e chi non le ama. Purtroppo o per fortuna, in alcuni ambiti sono molto comode perché, quando funzionano come vorremmo, riducono di molto il codice che dobbiamo scrivere per fare, ad esempio, una validazione.
Mi scrivi una regular expression per validare una stringa con al massimo 10 caratteri alfanumerici, alla cui terza posizione ci sia un numero?
il risultato, in base a quanto richiesto è decisamente centrato.
^[a-zA-Z0-9]{2}\d[a-zA-Z0-9]{0,7}$
Test di autovalutazione
Una parte del mio tempo la impiego ad insegnare e sicuramente non è quella pagata meglio, ma è certamente la più gratificante. La gioia che si ottiene potendo condividere con la generazione successiva le nostre conoscenze, vedendo gli studenti crescere, non ha prezzo.
Uno degli aspetti più fastidiosi, almeno per gli insegnanti, è la creazione e correzione dei test di valutazione alla fine delle sessioni di studio. Nella scuola dove insegno viene utilizzato Moodle, un ottimo strumento per la didattica a distanza, ma la creazione dei test è sempre un'attività noiosa e ripetitiva.
Quest'anno ho deciso di farmi aiutare da ChatGPT per creare i test di valutazione. Ho inserito il documento markdown con la sessione di studio che volevo valutare e ho chiesto a ChatGPT di generare l'XML, in formato Moodle, di 30 quiz a risposta multipla, focalizzati su quanto presente nel documento fornito.
Il risultato è stato molto soddisfacente: ho ottenuto un file XML pronto per essere importato in Moodle, con domande e risposte generate automaticamente da ChatGPT. Un esempio è contenuto nel mio corso CSS:
https://github.com/matteobaccan/CorsoCSS
Capite però quanto possa essere utile un modello LLM per poter generare dei test di autovalutazione ogni volta che ne abbiamo bisogno.
Conclusioni
Quando si approccia un LLM è importante capire che non si tratta di una bacchetta magica che risolve tutti i problemi, ma di uno strumento che può aiutarci a velocizzare il lavoro e a migliorare la qualità del codice.
Scrivere un prompt e sperare che, per magia, quello che abbiamo nella nostra testa sia magicamente convertito in codice esattamente come serve a noi, è qualcosa di impensabile. Allo stesso modo, pensare che esista un solo strumento di AI che risolva tutti i nostri problemi, ad ora, è un approccio sbagliato.
Il modo migliore con cui un programmatore dovrebbe approcciare un LLM è quello di capire quali sono i limiti dello strumento e quali sono i contesti in cui può essere utilizzato con successo, iniziando a catalogare mentalmente questi ambiti in modo che per ognuno di essi esista una tecnica o uno strumento che possa essere utilizzato efficacemente.
Allo stesso modo è impensabile poter riunire in un solo prompt un progetto: è molto più efficace dividere il progetto in parti e chiedere al modello di lavorare su queste parti, in modo da ottenere un risultato migliore e più preciso.
Anche nei contesti in cui un modello non è in grado di generare del codice, è molto probabile che sia in grado di documentarlo, commentarlo, tradurlo e fare molte altre operazioni che possono velocizzare il nostro lavoro.
Non siamo ancora in un momento storico in cui questi tool sono in grado di fare tutto in modo automatico, ma un programmatore che sa come utilizzarli può ottenere dei risultati molto buoni e migliorare la qualità del suo lavoro.
Migliorare la propria qualità vuol dire essere sempre più efficaci e toglierci dalla testa il cappello da junior per metterci quello più pesante e sgualcito di un programmatore senior.