juin
2011
Pour introduire le système d’exceptions de dodo (je préfère parler d’événements) je propose de s’atteler à une tâche simple, la traduction d’une méthode Java gérant les exceptions dans le langage dodo.
Sans plus d’ambages, voici la méthode en question:
private void openStore() { try { store = RecordStore.openRecordStore("CALCULATOR", true); for (int recordId = 1, last = store.getNextRecordID(); recordId < last; recordId++) { byte[] record = null; try { record = store.getRecord(recordId); } catch (InvalidRecordIDException x) { // record was deleted } if (record == null) { continue; } Statement stmt = new Statement(recordId, record); try { Compiler compiler = new Compiler(stmt.text); Compiler.Expression expr = compiler.compile(); if (expr instanceof Compiler.Declaration) { ((Compiler.Declaration)expr).declare(bindings); } stmt.state = Statement.COMPILED; } catch (CompileError x) { stmt.state = Statement.ERROR; } statements.addElement(stmt); } } catch(Exception x) { } }
On note trois blocs try
, le plus extérieur d’entre eux se bornant à avaler toutes les exceptions qui pourraient échapper la partie intérieure.
Commençons par créer une méthode dodo. Cette méthode a des effets de bord, on ne peut donc pas recourir à la création d’une fonction ordinaire.
Pour mémoire une méthode dodo commence par method
, suivi de son nom qui doit commencer en majuscule car l’on déclare un nouveau type. Dans notre cas on n’a pas besoin de variables ou de fonctions auxiliaires, alors le constructeur n’est pas défini dans le bloc de la méthode.
On écrit donc:
method OpenStore() { ... }
Maintenant écrivons le corps de la méthode. En première approximation nous allons essayer de reproduire exactement le code Java.
method OpenStore() { try { .store = RecordStore.OpenRecordStore("CALCULATOR", true) loop for (int recordId, last = (1, store.getNextRecord); recordId < last; ++.recordId) { byte[] record = null try { .record = store.getRecord(recordId) } ... }
Pour affecter une valeur à la variable store, il faut en prendre sa référence; cela est marqué par un point devant son nom. Dans un effort pour réduire le nombre de noms réservés en dodo, toutes les boucles commencent par loop
.
À ce point on arrive au premier bloc catch
du programme. Mais dodo n’autorise pas à écrire un bloc catch
à l’intérieur d’un autre bloc. Comment traduire le programme en dodo?
Regardons encore ce que fait le bloc catch en question dans le programme original:
} catch (InvalidRecordIDException x) { // record was deleted }
Ce bloc ne fait rien, il se contente d’avaler silencieusement une exception InvalidRecordIDException. En assumant que getRecord
ne jette pas une exception différente, on peut considérer tout ce qui suit comme faisant part d’un bloc finally
.
Cela tombe bien, en dodo ce qui suit un bloc try
est l’équivalent d’un bloc finally
. Donc on peut continuer en ignorant le bloc catch
:
method OpenStore() { try { .store = RecordStore.OpenRecordStore("CALCULATOR", true) loop for (int recordId, last = (1, store.getNextRecord); recordId < last; ++.recordId) { byte[] record = null try { .record = store.getRecord(recordId) } case if (record = null) { continue() } def stmt = Statement(recordId, record) ... }
Ici on construit un nouveau Statement avec les paramètres de constructeur (recordId, record). Notez que l’on utilise pas new
devant un constructeur. C’est ce qui permet aux méthodes d’être appelées comme des fonctions ordinaires, même si elles sont déclarées comme des types. De plus je n’ai pas spécifié le type de stmt
. Si ce type correspond au type de retour de l’expression, il suffit d’utiliser « def
» et dodo se charge de donner le type approprié à la variable.
On arrive maintenant à un nouveau bloc try
. Cela ne devrait pas poser de difficulté particulière:
method OpenStore() { try { .store = RecordStore.OpenRecordStore("CALCULATOR", true) loop for (int recordId, last = (1, store.getNextRecord); recordId < last; ++.recordId) { byte[] record = null try { .record = store.getRecord(recordId) } case if (record = null) { continue() } def stmt = Statement(recordId, record) try { def compiler = Compiler(stmt.text) def expr = compiler.compile case if (expr ~ ?Compiler.Declaration): expr.Declare(bindings). .stmt.state = Statement.compiled } ... }
On retrouve à nouveau le motif def variable = Constructeur(...)
, une notation à la simplicité bienvenue par rapport à Java. Le mot-clef case
est utilisé pour tous les branchements conditionnels comme loop
pour les boucles. Dodo a aussi la forme if ( ) ... else
mais elle ne s’utilise que pour les expressions, l’équivalent de ( )? ... :
en Java.
Le test du type de la variable se fait par correspondance de patron (pattern matching), où le patron est ici introduit par « ?
» suivi du nom du type. On peut aussi insérer d’autres critères avant le point d’interrogation, par exemple pour tester la valeur d’un attribut de la variable.
Nous voici donc à nouveau devant un bloc catch
qui suit un bloc try
. Cette fois-ci peut-on l’ignorer comme s’il n’existait pas? Pas vraiment, cette fois-ci le bloc contient une instruction. Mais comme on arrive à la fin de la fonction il est peut-être raisonnable de décaler le bloc catch
un peu plus bas, en-dehors des autres blocs. Voici ce que cela donne:
method OpenStore() { try { .store = RecordStore.OpenRecordStore("CALCULATOR", true) loop for (int recordId, last = (1, store.getNextRecord); recordId < last; ++.recordId) { byte[] record = null try { .record = store.getRecord(recordId) } case if (record = null) { continue() } def stmt = Statement(recordId, record) try { def compiler = Compiler(stmt.text) def expr = compiler.compile case if (expr ~ ?Compiler.Declaration): expr.Declare(bindings). .stmt.state = Statement.compiled } statements.AddElement(stmt) } } catch (event: ?CompileError) { .stmt.state = Statement.error } ... }
L’exception est comparée au patron fourni à catch
, et si elle correspond le bloc est exécuté.
On voit un problème ici: l’instruction du catch
fait appel à la variable stmt
, qui n’est pas déclarée au plus haut niveau de la méthode. Mais dodo ne s’en formalise pas; si l’exception se produit là où la variable stmt
est déclarée, alors l’instruction est valide. Ailleurs l’instruction produit une nouvelle exception. Si nécessaire, on peut restreindre le bloc catch
aux exceptions jetées après une étiquette de notre choix ce qui permet d’éviter les variables non déclarées.
Il ne manque plus qu’à ajouter le bloc catch
du try
englobant qui avale toutes les exceptions. On obtient:
method OpenStore() { try { .store = RecordStore.OpenRecordStore("CALCULATOR", true) loop for (int recordId, last = (1, store.getNextRecord); recordId < last; ++.recordId) { byte[] record = null try { .record = store.getRecord(recordId) } case if (record = null) { continue() } def stmt = Statement(recordId, record) try { def compiler = Compiler(stmt.text) def expr = compiler.compile case if (expr ~ ?Compiler.Declaration): expr.Declare(bindings). .stmt.state = Statement.compiled } statements.AddElement(stmt) } } catch (event: ?CompileError) { .stmt.state = Statement.error } catch { } }
Certes ce programme correspond bien au programme Java initial, mais l’on a pas profité de toutes les possibilités de dodo en terme de gestion d’exception. Pour commencer, à quoi bon écrire un bloc try
si il n’y a pas d’instructions à exécuter par la suite? Puisqu’un bloc catch
n’est pas attaché à un bloc try
particulier, la gestion d’exception intervient même s’il n’y a pas de try
. Enlevons donc ce bloc try
superflu:
method OpenStore() { .store = RecordStore.OpenRecordStore("CALCULATOR", true) loop for (int recordId, last = (1, store.getNextRecord); recordId < last; ++.recordId) { byte[] record = null try { .record = store.getRecord(recordId) } case if (record = null) { continue() } def stmt = Statement(recordId, record) try { def compiler = Compiler(stmt.text) def expr = compiler.compile case if (expr ~ ?Compiler.Declaration): expr.Declare(bindings). .stmt.state = Statement.compiled } statements.AddElement(stmt) } catch (event: ?CompileError) { .stmt.state = Statement.error } catch { } }
Ensuite on voit là un motif intéressant:
def stmt = Statement(recordId, record) try { ... } statements.AddElement(stmt)
Ici on initialise la variable stmt
, on exécute un bloc try
, puis finalement on ajoute la variable à une liste à l’aide d’une seule instruction. Cette dernière s’exécute aussi bien en cas de succès qu’en cas d’exception.
Dodo propose une notation spéciale pour ce motif qui rend le programme plus succinct. L’instruction finale remonte au niveau de l’instruction d’initialisation, et tout ce qui suit fait partie d’un bloc try
implicite. Je l’utilise dans le programme ci-dessous.
method OpenStore() { .store = RecordStore.OpenRecordStore("CALCULATOR", true) loop for (int recordId, last = (1, store.getNextRecord); recordId < last; ++.recordId) { byte[] record = null try { .record = store.getRecord(recordId) } case if (record = null) { continue() } def stmt = Statement(recordId, record) ...> statements.AddElement(stmt) def compiler = Compiler(stmt.text) def expr = compiler.compile case if (expr ~ ?Compiler.Declaration): expr.Declare(bindings). .stmt.state = Statement.compiled } catch (event: ?CompileError) { .stmt.state = Statement.error } catch { } }
Enfin, on peut se débarrasser du dernier try
en remarquant que le continue
ne s’exécute que si getRecord
a jeté une exception, auquel cas un bloc catch
peut intervenir. Au lieu de placer un continue
dans ce bloc catch
, je vais utiliser resume
avec une étiquette judicieusement placée pour passer à l’itération suivante de la boucle.
method OpenStore() { .store = RecordStore.OpenRecordStore("CALCULATOR", true) loop for (int recordId, last = (1, store.getNextRecord); @cond recordId < last; ++.recordId) { def record = store.getRecord(recordId) def stmt = Statement(recordId, record) ...> statements.AddElement(stmt) def compiler = Compiler(stmt.text) def expr = compiler.compile case if (expr ~ ?Compiler.Declaration): expr.Declare(bindings). .stmt.state = Statement.compiled } catch (event: ?InvalidRecordIDException) { # record was deleted ++.recordId resume @cond } catch (event: ?CompileError) { .stmt.state = Statement.error } catch { } }
En obligeant les blocs catch
à se situer en-dehors des autres blocs, et à l’aide d’autres astuces de notation, dodo permet d’écrire des instructions qui ne se mélangent pas avec la gestion des cas exceptionnels. Le programme y gagne en clarté et en concision.