par Laurent Bloch
Dans un article précédent je rendais compte d’un étude menée scientifiquement pour évaluer l’aide que pourrait apporter, ou pas, l’IA au travail de 16 développeurs expérimentés pour résoudre 246 problèmes de maintenance relatifs aux projets dont ils étaient familiers depuis des années : la conclusion était que l’IA leur faisait plutôt perdre du temps.
Ici je vais décrire de façon beaucoup plus empirique mon expérience avec l’IA (ChatGPT en l’occurrence) pour m’initier à un langage de programmation avec lequel je débute, TypeScript. TypeScript est une couche objet ajoutée à JavaScript par des ingénieurs de Microsoft (sous la direction d’Anders Hejlsberg) pour en rendre la programmation plus robuste et plus lisible (ce sont des euphémismes). Soyez charitables pour les faiblesses que j’exhibe ! En tout cas ChatGPT m’a bien aidé : c’est comme travailler sous les yeux d’un collègue plus expérimenté qui aperçoit tout de suite les erreurs grossières de débutant.
Première question
// Bonsoir ChatGPT,
// Voici, ci-dessous, un code TypeScript correct, constitué de deux classes et d’un certain nombre de méthodes. Peux-tu répartir les classes et les méthodes dans autant de NameSpaces, puis placer chaque NameSpace dans un fichier, tels que le compilateur tsc puisse assembler le code et le soumettre à la machine JavaScript ?
// Une classe pour le type nœud : N
class N<T> {
// Valeur du nœud
public data: T;
// Référence du nœud suivant
public next: N<T> | null = null;
// Création d'un nœud avec la valeur donnée
constructor(value: T) {
this.data = value;
}
}
// Une classe pour la liste :
export class ListeSimple<T> {
// Nœud de tête :
public head: N<T> | null = null;
// Ajouter un nœud en tête :
prepend(data: T): void {
const newNode = new N(data); // crée un nouveau nœud, valeur = data
newNode.next = this.head; // le pointeur next du nouveau nœud pointe sur l'actuel nœud de tête
this.head = newNode; // Désormais le nœud de tête est le nouveau nœud
}
// Ajouter un nœud à la fin :
append(data: T): void {
const newNode = new N(data);
if (!this.head) { // Si personne en tête : liste vide, le nouveau vient en tête
this.head = newNode;
return;
}
let current = this.head;
while (current.next) { // Parcourir la liste jusqu'à trouver le dernier nœud,
current = current.next; // reconnaissable à ce que son pointeur next vaut
} // null
current.next = newNode; // Et alors le nouveau vient en queue
}
// Retirer le nœud de tête :
deleteHead(): void {
if (this.head) {
this.head = this.head.next; // Le second prend la place
}
}
// Retirer le nœud de fin :
deleteTail(): void {
if (!this.head) return; // Si liste vide retour
if (!this.head.next) {
this.head = null; // Si un seul élément on l'oublie
return;
}
let current = this.head;
while (current.next && current.next.next) { // On parcourt
current = current.next; // la liste jusqu'au dernier
}
current.next = null; // Quand on l'a trouvé on l'oublie
}
// Retirer le premier nœud d'une valeur donnée :
delete(data: T): void {
if (!this.head) return; // Si liste vide, adios
if (this.head.data === data) { // Si le premier est le bon
this.head = this.head.next; // on l'oublie
return;
}
let current = this.head;
while (current.next) { // Sinon on parcourt la liste
if (current.next.data === data) { // jusqu'à trouver
current.next = current.next.next; // si bon on supprime
return;
}
current = current.next; // Tant que pas trouvé, on parcourt
}
}
// Trouver un nœud d'une valeur donnée :
find(data: T): N<T> | null {
if (!this.head) return null;
let current: N<T> | null = this.head;
while (current) {
if (current.data === data) return current;
current = current.next;
}
return null; // Si pas trouvé
}
// Liste de toutes les valeurs
traverse(): void {
let current = this.head;
while (current) {
console.log(current.data);
current = current.next;
}
}
// Insérer un nœud à une position donnée :
// insertAt(pos: number, data: T): void {}
}
const uneListePersonnages = new ListeSimple
uneListePersonnages.prepend("Tintin") ;
uneListePersonnages.prepend("Haddock") ;
uneListePersonnages.prepend("Milou") ;
console.log(uneListePersonnages.traverse()) ;
Réponse :
Parfait 👍 Ton code est bien structuré. Pour répondre à ta demande, je vais te montrer comment répartir les classes dans des namespaces, puis les mettre chacune dans un fichier séparé afin que tsc puisse assembler le tout correctement.
1. Organisation proposée
On va séparer en deux fichiers principaux :
N.ts → contient le namespace NodeSpace avec la classe N
ListeSimple.ts → contient le namespace ListeSpace avec la classe ListeSimple (qui dépend de N)
Enfin, on aura un fichier main.ts pour tester l’ensemble.
2. Fichier : N.ts
namespace NodeSpace {
export class N<T> {
// Valeur du nœud
public data: T;
// Référence du nœud suivant
public next: N<T> | null = null;
// Création d'un nœud avec la valeur donnée
constructor(value: T) {
this.data = value;
}
}
}
3. Fichier : ListeSimple.ts
/// <reference path="./N.ts" />
namespace ListeSpace {
export class ListeSimple<T> {
// Nœud de tête :
public head: NodeSpace.N<T> | null = null;
// Ajouter un nœud en tête :
prepend(data: T): void {
const newNode = new NodeSpace.N(data);
newNode.next = this.head;
this.head = newNode;
}
// Ajouter un nœud à la fin :
append(data: T): void {
const newNode = new NodeSpace.N(data);
if (!this.head) {
this.head = newNode;
return;
}
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
// Retirer le nœud de tête :
deleteHead(): void {
if (this.head) {
this.head = this.head.next;
}
}
// Retirer le nœud de fin :
deleteTail(): void {
if (!this.head) return;
if (!this.head.next) {
this.head = null;
return;
}
let current = this.head;
while (current.next && current.next.next) {
current = current.next;
}
current.next = null;
}
// Retirer le premier nœud d'une valeur donnée :
delete(data: T): void {
if (!this.head) return;
if (this.head.data === data) {
this.head = this.head.next;
return;
}
let current = this.head;
while (current.next) {
if (current.next.data === data) {
current.next = current.next.next;
return;
}
current = current.next;
}
}
// Trouver un nœud d'une valeur donnée :
find(data: T): NodeSpace.N<T> | null {
if (!this.head) return null;
let current: NodeSpace.N<T> | null = this.head;
while (current) {
if (current.data === data) return current;
current = current.next;
}
return null;
}
// Liste de toutes les valeurs
traverse(): void {
let current = this.head;
while (current) {
console.log(current.data);
current = current.next;
}
}
}
}
4. Fichier : main.ts
/// <reference path="./N.ts" />
/// <reference path="./ListeSimple.ts" />
const uneListePersonnages = new ListeSpace.ListeSimple<string>();
uneListePersonnages.prepend("Tintin");
uneListePersonnages.prepend("Haddock");
uneListePersonnages.prepend("Milou");
uneListePersonnages.traverse();
5. Compilation avec tsc
Exécute :
tsc —outFile dist/app.js main.ts
⚡ Ici on utilise —outFile car on travaille avec des namespaces (et pas avec des modules ES6 import/export). Le compilateur va assembler N.ts, ListeSimple.ts et main.ts dans un seul fichier dist/app.js exploitable en JavaScript.
👉 Veux-tu que je te propose aussi une version avec modules ES6 (export / import), plus moderne que les namespace + ///
Oui.
Seconde réponse, meilleure :
ChatGPT a dit :
Parfait 👌 allons-y avec la version moderne en modules ES6.
Ici, pas de namespace ni de ///
1. Fichier N.ts
export class N<T> {
// Valeur du nœud
public data: T;
// Référence du nœud suivant
public next: N<T> | null = null;
// Création d'un nœud avec la valeur donnée
constructor(value: T) {
this.data = value;
}
}
2. Fichier ListeSimple.ts
import { N } from "./N";
export class ListeSimple<T> {
// Nœud de tête :
public head: N<T> | null = null;
// Ajouter un nœud en tête :
prepend(data: T): void {
const newNode = new N(data);
newNode.next = this.head;
this.head = newNode;
}
// Ajouter un nœud à la fin :
append(data: T): void {
const newNode = new N(data);
if (!this.head) {
this.head = newNode;
return;
}
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
// Retirer le nœud de tête :
deleteHead(): void {
if (this.head) {
this.head = this.head.next;
}
}
// Retirer le nœud de fin :
deleteTail(): void {
if (!this.head) return;
if (!this.head.next) {
this.head = null;
return;
}
let current = this.head;
while (current.next && current.next.next) {
current = current.next;
}
current.next = null;
}
// Retirer le premier nœud d'une valeur donnée :
delete(data: T): void {
if (!this.head) return;
if (this.head.data === data) {
this.head = this.head.next;
return;
}
let current = this.head;
while (current.next) {
if (current.next.data === data) {
current.next = current.next.next;
return;
}
current = current.next;
}
}
// Trouver un nœud d'une valeur donnée :
find(data: T): N<T> | null {
if (!this.head) return null;
let current: N<T> | null = this.head;
while (current) {
if (current.data === data) return current;
current = current.next;
}
return null;
}
// Liste de toutes les valeurs
traverse(): void {
let current = this.head;
while (current) {
console.log(current.data);
current = current.next;
}
}
}
3. Fichier main.ts
import { ListeSimple } from "./ListeSimple";
const uneListePersonnages = new ListeSimple<string>();
uneListePersonnages.prepend("Tintin");
uneListePersonnages.prepend("Haddock");
uneListePersonnages.prepend("Milou");
uneListePersonnages.traverse();
4. Compilation avec tsc
Crée un tsconfig.json (si tu n’en as pas déjà un) :
{
"compilerOptions": {
"target": "ES6",
"module": "ES6",
"moduleResolution": "node",
"outDir": "dist",
"rootDir": "./",
"strict": true
},
"include": ["*.ts"]
}
Ensuite, compile :
tsc
Cela va générer dans dist/ :
N.js
ListeSimple.js
main.js
👉 Petite différence d’usage :
Avec namespaces, on regroupe tout en un seul fichier avec —outFile.
Avec modules ES6, chaque fichier génère son propre .js, et on les relie via import / export.
Suite de la question :
Bonjour ChatGPT, j’ai créé en TypeScript une classe de liste associative :
Bonjour ! Veux-tu me montrer le code de ta classe TypeScript de liste associative ? Comme ça je pourrai t’aider à l’améliorer, la corriger, ou l’adapter selon ton besoin.
// La classe ListeAssoc
import { Nassoc } from "./Nassoc";
export class ListeAssoc<Tkey, Tvalue> {
// Noeud de tête :
public head: Nassoc<Tkey, Tvalue> | null = null;
// Ajouter un noeud en tête :
prepend(theKey: Tkey, theValue: Tvalue): void {
const newNode = new Nassoc(theKey, theValue);
newNode.next = this.head;
this.head = newNode;
}
// Ajouter un noeud à la fin :
append(theKey: Tkey, theValue: Tvalue): void {
const newNode = new Nassoc(theKey, theValue);
if (!this.head) {
this.head = newNode;
return;
}
Suite de la question :
Vous avez dit (je vouvoie encore ChatGPT)
:
En fait on n’a besoin que de prepend. Et je crée un tableau de listes pour créer une table de hash, ainsi :
// La classe HashTable
import { ListeAssoc } from "../Listes/ListeAssociative/ListeAssoc";
type Tuple<
T,
N extends number,
R extends readonly T[] = [],
> = R['length'] extends N ? R : Tuple<T, N, readonly [T, ...R]>;
export class HashTable<ListeAssoc, n extends number> {
public aTable: Tuple<ListeAssoc, n> | null = null;
init(n) {
const laListeVide = new ListeAssoc<string, number>();
for (let pas = 0; pas < n; pas++){
this[pas] = laListeVide;
}
}
}
Réponse :
ChatGPT a dit :
Merci pour le partage ! 🙂
Là tu as deux morceaux : une liste associative chaînée (ListeAssoc) et une table de hachage (HashTable). Je vois quelques points qui risquent de poser problème dans ton code actuel :
1. La déclaration de HashTable
export class HashTable
Ici tu appelles ton paramètre générique ListeAssoc, mais ce nom est déjà pris par ta classe importée ListeAssoc. En fait, tu veux plutôt paramétrer les types clé et valeur, pas redéclarer une classe.
👉 Je te propose de renommer les génériques en Tkey, Tvalue et éventuellement N pour la taille.
2. La construction de aTable
public aTable : Tuple
Ici, tu veux un tableau de listes associatives. Or Tuple
3. Ton init
init(n) {
const laListeVide = new ListeAssoc<string, number>();
for (let pas = 0; pas < n; pas++){
this[pas] = laListeVide;
}
}
Problème : tu réutilises la même instance de ListeAssoc pour toutes les cases. Résultat : tout ton tableau pointe vers une seule liste partagée.
Il faut recréer une nouvelle ListeAssoc à chaque itération.
Version corrigée
import { ListeAssoc } from "../Listes/ListeAssociative/ListeAssoc";
export class HashTable<Tkey, Tvalue> {
public aTable: Array<ListeAssoc<Tkey, Tvalue>>;
constructor(private size: number) {
this.aTable = [];
for (let i = 0; i < size; i++) {
this.aTable[i] = new ListeAssoc<Tkey, Tvalue>();
}
}
// Exemple de fonction de hachage très simple (à améliorer selon ton besoin)
private hash(key: string): number {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = (hash + key.charCodeAt(i)) % this.size;
}
return hash;
}
set(key: Tkey & string, value: Tvalue): void {
const index = this.hash(key);
this.aTable[index].prepend(key, value);
}
get(key: Tkey & string): Tvalue | undefined {
const index = this.hash(key);
let node = this.aTable[index].head;
while (node) {
if (node.key === key) return node.value;
node = node.next;
}
return undefined;
}
}
👉 Ici :
HashTable prend des types génériques clé et valeur (Tkey, Tvalue).
On construit un tableau de ListeAssoc indépendantes.
J’ai ajouté une fonction de hachage simple + des méthodes set et get pour illustrer.
Troisième question :
Re-bonjour,
Voilà, j’ai déclaré une classe HashTable selon tes conseils :
// La classe HashTable
import { ListeAssoc } from "../Listes/ListeAssociative/ListeAssoc";
export class HashTable<Tkey, Tvalue> {
public aTable: Array<ListeAssoc<Tkey, Tvalue>>;
constructor(private size: number) {
this.aTable = [];
for (let i = 0; i < size; i++) {
this.aTable[i] = new ListeAssoc<Tkey, Tvalue>;
}
}
// Exemple de fonction de hachage très simple
private hash(key: string): number {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = (hash + key.charCodeAt(i)) % this.size;
}
return hash;
}
set(key: Tkey & string, value: Tvalue): void {
const index = this.hash(key);
this.aTable[index].prepend(key, value);
}
get(key: Tkey & string): Tvalue | undefined {
const index = this.hash(key);
let node = this.aTable[index].head;
while (node) {
if (node.key === key) return node.value;
node = node.next;
}
return undefined;
}
}
Je crée une instance :
const myHashTable = new HashTable(6) ;
J’inscris des personnages dans la table :
myHashTable.set("Tournesol", 18) ;
myHashTable.set("Dupont", 19) ;
Quand j’interroge j’ai la bonne réponse :
console.log("Dupont : ", myHashTable.get("Dupont")) ;
– > Dupont : 19
Mais si j’écris :
for (let pas = 0; pas < 6; pas++){
let current = myHashTable[pas];
while (current){
console.log(pas, current.aTable);
current = current.next;
}
}
Je n’ai pas de réponse, et si j’écris :
for (let pas = 0; pas < 6; pas++){
let current = myHashTable[pas];
while (current){
console.log(pas, current.aTable);
current.ListeAssoc.traverse;
current = current.next;
}
}
on me dit :
current.ListeAssoc.traverse;
^
TypeError: Cannot read properties of undefined (reading 'traverse')
at Object.<anonymous> (/home/lbloch/nextcloud/Travaux/Mes-Livres.own/AlgoTypeScript/Programmes/Structures/HashTables/main.js:31:28)
at Module._compile (node:internal/modules/cjs/loader:1356:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1414:10)
at Module.load (node:internal/modules/cjs/loader:1197:32)
at Module._load (node:internal/modules/cjs/loader:1013:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:128:12)
at node:internal/main/run_main_module:28:49
Node.js v18.19.1
Qu’est-ce qui ne va pas ?
Réponse :
Je vois tout de suite où ça coince 👍
Dans ta boucle tu fais :
for (let pas = 0; pas < 6; pas++){
let current = myHashTable[pas];
while (current){
console.log(pas, current.aTable);
current = current.next;
}
}
👉 problème :
myHashTable est un objet de type HashTable, pas un tableau.
L’attribut qui contient tes listes est aTable, donc tu dois accéder à myHashTable.aTable[pas].
Ensuite, chaque case de aTable est une ListeAssoc, qui a un champ head pointant vers le premier Nassoc.
En gros, myHashTable[pas] n’existe pas → ça vaut undefined.
✅ Correction
Pour parcourir toutes les cases et tous les nœuds, fais comme ceci :
for (let pas = 0; pas < 6; pas++) {
let current = myHashTable.aTable[pas].head;
while (current) {
console.log(pas, current.key, current.value);
current = current.next;
}
}
Autres points :
Dans ton constructeur, il manque des () à l’appel du constructeur ListeAssoc :
this.aTable[i] = new ListeAssoc
(sinon tu construis une référence vers la classe, pas une instance).
current.ListeAssoc.traverse n’a aucun sens :
current est un Nassoc, il a seulement key, value et next.
Si tu veux faire une méthode traverse, elle doit être définie dans ListeAssoc (et tu l’appellerais avec myHashTable.aTable[pas].traverse()).