Sommaire
Et voilà, à peine développé et déjà un fork :)
Suite au journal de mzf, j'ai décidé de porter le taptempo en Ada.
Pourquoi faire ? Juste pour le plaisir :)
Et puis aussi parce que le logiciel était suffisamment court pour faire un portage rapide et montrer différents aspects d'Ada sur un programme déjà existant en C++.
Après une version Rust, voici donc, comme promis, la version Ada et sa petite explication… Enfin, deux versions.
Première version
La première version que j'ai décidé de vous présenter est un portage direct du code C++ avec quelques ajouts typiquement Ada-esques. C'est facile, le code est là.
La structure
Le programme est découpé en plusieurs morceaux:
- la procédure adataptempo faisant office de programme principal
- le package Options, dépendant de la bibliothèque Parse_Args, permettant de gérer les options du programme
- le package TapTempo définissant les types nécessaires ainsi que le fonctionnement complet du taptempo
Les types
C'est par ce dernier package que je vais commencer.
Il définit les types suivants:
type Tap_Tempo is private;
subtype Sample_Size is Count_Type;
type Seconds is new Positive;
type Precision is range 0 .. 5;
Les types Sample_Size, Seconds et Precision permettent de spécifier les contraintes du problème. On voit là un des avantages par rapport au type size_t, les types étant d'une part, incompatibles entre eux et d'autre part, beaucoup plus contraints.
Cela permet notamment de se passer des tests suivants, ces valeurs n'étant pas dans la plage acceptable
if(this->sampleSize == 0)
{
this->sampleSize = 1;
}
if(this->resetTimeInSecond == 0)
{
this->resetTimeInSecond = 1;
}
Le type Precision se passe d'explication :)
Enfin, le type Tap_Tempo est déclaré privé et donc sa définition n'est pas accessible à l'extérieur du package. Il faut donc définir une sorte de constructeur. C'est ce qui est fait au travers de la fonction Build.
function Build
(Size : Sample_Size := 5;
Reset_Time_In_Second : Seconds := 5;
Precision_Needed : Precision := 0) return Tap_Tempo;
Mais du coup, c'est quoi la définition de notre objet privé ?
-- Time vector type
package Time_Vectors is new Ada.Containers.Vectors(Index_Type => Positive,
Element_Type => Time);
use Time_Vectors;
type Tap_Tempo is record
Size : Sample_Size := 1;
Reset_Time_In_Second : Seconds := 1;
Precision_Needed : Precision := 1;
Time_Vector : Time_Vectors.Vector;
end record;
Une petite explication sur les premières lignes s'impose.
Il s'agit de l'instanciation d'un package générique standard pour la gestion des vecteurs.
Pourquoi vecteur et pas queue ? Parce qu'Ada ne fournit pas en standard de queue simple mais uniquement des queues synchronisées dans le cadre de la programmation multitâches ce qui impose beaucoup trop de contraintes pour notre cas simple.
Ensuite, le Tap_Tempo n'est finalement qu'un simple enregistrement, même pas objet :D
Et c'est sur ce type que l'on définit la procédure Run
procedure Run (Tempo : in out Tap_Tempo);
On remarquera au passage l'utilisation de in out qui permet de voir que l'on modifiera l'objet dans la procédure, le vecteur en fait.
La procédure Run
Il s'agit d'un simple portage ligne à ligne du code C++ mais quelques petits zooms sur ce code permettront de voir une ou deux spécificités Ada.
Une première spécificité est liée au système de types. En effet, la procédure Get_Immediate renvoie un caractère et non un simple entier. Cela permet donc d'expliciter la clause de sortie de boucle :
exit when (Key = Ada.Characters.Latin_1.LF);
Personnellement, je préfère ça, sans aucune arrière-pensée, à
} while (i != 10);
Deuxième spécificité qui peut surprendre le développeur C/C++ voire Java, le test permettant de savoir que l'on a trop attendu
if (not Is_Empty (Tempo.Time_Vector)
and then Is_Reset_Time_Elapsed(Tempo,Current_Time,First_Element (Tempo.Time_Vector)))
En Ada, dans le cas d'une opération logique, tous les termes sont évalués ce qui pose ici problème car le premier a pour but d'éviter de faire le second test. Finalement, on dit bien ce que l'on fait à savoir si…et alors….
Il y aurait encore des choses à dire sur tout ce package mais bon, ce n'est qu'un journal et il est tard alors je répondrai aux questions si besoin dans les commentaires :D
Du coup, voyons ce que l'on peut faire de plus.
Seconde version
Cette seconde version part d'un constat simple, le taptempo utilise l'entrée standard qui n'est pas protégée pour les accès concurrents. De plus, quel est l'intérêt d'avoir plusieurs instances du taptempo ?
J'aurai tendance à dire aucune… Surtout que ça m'arrange :D
En C++, la technique est d'utiliser le patron de conception du singleton. Il se trouve que celui-ci est très simple à implémenter en Ada.
En effet, contrairement aux namespaces C++, le package Ada est une unité de compilation et d'encapsulation à part entière. Ainsi, il est possible de "cacher" un état dans le corps d'un package et a fortiori, notre file.
package body TapTempo is
-- Time vector type
package Time_Vectors is new Ada.Containers.Vectors(Index_Type => Positive,
Element_Type => Time);
use Time_Vectors;
Time_Vector : Time_Vectors.Vector;
Il suffit ensuite de déplacer les paramètres de configuration dans la méthode Run comme suit
procedure Run
(Size : Sample_Size := 5;
Reset_Time_In_Second : Seconds := 5;
Precision_Needed : Precision := 0);
Du coup, plus besoin de construction d'un objet Tap_Tempo dans la procédure principale adataptempo mais on a seulement besoin d'appeler Run.
Voilà, il n'y a pas de révolution dans cette seconde version mais le but était juste de montrer l'utilité des packages et leur capacité à fournir de l'encapsulation et donc une portée en plus d'un espace de noms.
Bien sûr, vous trouverez tout ce code ici.
Conclusion
J'ai pris plaisir à faire ce petit portage qui m'a, au passage, permis d'essayer la bibliothèque Parse_Args pour la gestion des options.
En espérant que ce "petit" journal vous ait plu… Place aux commentaires !
# Petit oubli
Posté par Blackknight (site web personnel, Mastodon) . Évalué à 4.
Bon, j'ai oublié de dire que je n'ai pas fait la partie internationalisation, histoire de vous fournir rapidement un truc à vous mettre sous la dent ;)
Du coup, entre l'absence d'internationalisation et de licence dans chaque fichier, il manque forcément quelques lignes dans les stats qui suivent
Version 1
Version 2
Bon, le gain est pas énorme :D
# Bravo !
Posté par mzf (site web personnel) . Évalué à 3.
Bravo pour le portage, j'ai appris pas mal de choses sur Ada !
Concernant la définition des types, que se passe t'il quand l'utilisateur rentre une valeur hors cadre ?
Je ne connais pas très bien Ada, du coup je n'arrive pas à comprendre où est le code qui permet de mettre par exemple Sample_Size à 1 si celui-ci est 0.
Une erreur est-elle lancée à la place ?
[^] # Re: Bravo !
Posté par Blackknight (site web personnel, Mastodon) . Évalué à 3.
Lors de la saisie d'une valeur hors plage, le runtime Ada lance une exception de type CONSTRAINT_ERROR qu'il est tout à fait possible de catcher.
Ici, il n'est de toutes façons pas possible de saisir, en statique, 0 pour la précision ou le timeout, le compilateur annoncera la levée d'exception dans un warning ces derniers étant des types dont la première valeur est 1.
Dans le cas de ce programme, le filtrage des valeurs incorrectes est remonté à la gestion des options. Ainsi, le code suivant fournit tout ce qu'il faut pour gérer les valeurs hors plage :
Dans le cas simple du type Positive, c'est l'appel à Make_Positive_Option qui gère tout seul, enfin dans la lib Parse_Args, les valeurs hors-plage.
C'est rendu possible par le fait que les types embarquent leur bornes (par exemple Precision'Last retourne la dernière valeur valide du type Precision.
[^] # Re: Bravo !
Posté par mzf (site web personnel) . Évalué à 2.
Donc c'est l'avantage des types bornés et la magie de Parse_Args qui le permet.
Merci pour ces explications !
[^] # Re: Bravo !
Posté par Blackknight (site web personnel, Mastodon) . Évalué à 2.
Oui. Ainsi, la compilation du code suivant
renvoie le message suivant
Et à l'exécution, comme prévu et prévenu
[^] # Re: Bravo !
Posté par dinomasque . Évalué à 5.
Je me joint à cette louange.
Ce journal est didactique et intéressant. La qualité d'écriture le rend facile et agréable à lire.
BeOS le faisait il y a 20 ans !
[^] # Re: Bravo !
Posté par Blackknight (site web personnel, Mastodon) . Évalué à 3.
Merci mais là, mon ego en a pour la journée à s'en remettre :D
# Merci !
Posté par Boiethios (site web personnel) . Évalué à 1.
Ce langage a l'air intéressant, même si la syntaxe est déroutante pour moi. Ca commence à faire pas mal de fois que j'entends parler en bien de celui-ci, il faudra que je creuse tout ça.
Sinon, je suppose que je n'ai pas trop le choix, je devrai écrire un journal sur mon port en Rust :p
[^] # Re: Merci !
Posté par Axioplase ıɥs∀ (site web personnel) . Évalué à 3.
C'est la syntaxe du Pascal. Puis Ada a été créé en 1980, donc c'est un peu verbeux, mais c'est pas ésotérique non plus.
[^] # Re: Merci !
Posté par Blackknight (site web personnel, Mastodon) . Évalué à 1.
Ce qui explique que ce soit largement passé de mode
Heureusement, les IDE modernes permettent la completion.
[^] # Re: Merci !
Posté par Axioplase ıɥs∀ (site web personnel) . Évalué à 4. Dernière modification le 27 février 2018 à 23:02.
Et le C date de 1972. Passé de mode ? Je ne crois pas, non.
Le langage Ada a évolué aussi depuis ses débuts. C'est un langage concurrent et orienté objet. C'est pas suranné comme concepts, ça.
L'âge du langage explique pourquoi un programmeur qui ne connait que la syntax JS peut être surpris, mais c'est plutôt un signe d'ignorance
^W
de jeunesse. La syntaxe Pascal, c'est pas non plus celle d'APL… C'est lisible et compréhensible par n'importe qui.[^] # Re: Merci !
Posté par Blackknight (site web personnel, Mastodon) . Évalué à 3. Dernière modification le 27 février 2018 à 23:04.
Et c'est des fois dommage quand on voit ce que l'on essaye de lui faire faire.
T'inquiètes, tu prêches un converti et connaisseur mais je passe quand même pour un hurluberlu à faire de l'évangélisation Ada.
En tout cas, la note même de ce journal montre que ce n'est pas si recherché que ça comme langage au moins sur LinuxFr.
[^] # Re: Merci !
Posté par Blackknight (site web personnel, Mastodon) . Évalué à 4.
Exactement et personnellement, je l'attends avec impatience :)
Et surtout n'hésites pas à mettre plein d'explications parce que les macros et les map, ça reste souvent cryptique pour moi :D
Notamment
Mais c'est parce que je ne suis pas câblé fonctionnel ;)
[^] # Re: Merci !
Posté par Boiethios (site web personnel) . Évalué à 2.
Je pense que si je fais un journal, je ne vais pas garder ces lignes, c'est un peu moche.
Pour expliquer vite fait, le résultat de
reader.next()
est unOption<Result<String, Error>>
, c'est-à-dire qu'on pourrait avoir ou pas un résultat (Option est un type optionnel, commeMaybe
en Haskell). On n'a pas de résultat quand l'utilisateur fait un CTRL+D sur une ligne vide par exemple.Du coup, le
unwrap_or
sert à dire: "récupère le contenu de l'option, ou prend la valeurOk("q".into())
comme valeur par défaut si on n'a pas de valeur dedans".Donc on a un type
Result<String, Error>
à ce moment. Il vaut soitOk(String)
si tout s'est bien passé, soitErr(Error)
si il y a eu un problème.map_err
sert à changer le type dansErr
; etmap
sert à changer le type deOk
.Si on écrivait tout ça en plus verbeux, ça donnerait:
[^] # Re: Merci !
Posté par Blackknight (site web personnel, Mastodon) . Évalué à 2. Dernière modification le 27 février 2018 à 21:34.
Merci pour l'explication.
D'ailleurs, je préfère la version verbeuse, je la trouve plus lisible ;)
En tout cas, fais un journal, on verra la cote de chaque langage à la note de celui-ci :)
Pour l'instant, c'est C++ qui gagne mais je ne doute pas que vu la popularité actuelle de Rust, tu marques des points.
[^] # Re: Merci !
Posté par kantien . Évalué à 5. Dernière modification le 28 février 2018 à 18:41.
C'est parce que la pipeline est courte. Il faut voir cette notation pointée de Rust comme le pipe
|
du shell :reader.next() | unwrap_or | map_err | map
. Sur de longues pipelines, c'est plus simple à écrire et il vaut mieux laisser le compilateur inliner le tout plutôt que de le faire à la main (comme dans sa version verbeuse, qui contient d'ailleurs une erreur : le casNone
doit retournerOk(false)
de typeResult<Bool, String>
).En OCaml (j'étais bien obligé), on écrirait un code du genre :
Les patterns à base de
map
sont omniprésents dans le paradigme fonctionnel. Un type paramétrique à un paramètre (comme les options, les listes, les tableaux, les vecteurs…) est une fonction des types dans les types. Ainsi si j'ai une fonction qui transforme les objets du paramètre (disons desint
enstring
), je peux la lifter pour opérer sur le type paramétrique (un peu comme une composition de fonction, si tu veux). Exemples :Le type
Result
est lui un type paramétrique à deux paramètres : on peut donc faire unmap
soit sur le type en paramètre deOk
, soit sur celui en paramètre deErr
. Ce qui soit donne deux fonctionsmap
(comme en Rust), soit une fonctionmap_both
comme dans mon exemple en OCaml (oubimap
en Haskell) qui prend deux fonctions en paramètres (une pour chaque type).P.S : sinon sympa le journal, et comme depuis les derniers journaux sur la virgule flottante j'ai installé GNAT et GNAT Programming Studio je vais pouvoir regarder un code ADA idiomatique et jouer avec :-) (ravi de voir au passage qu'il y a des développeurs ADA qui comprennent l'encapsulation ;-).
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
[^] # Re: Merci !
Posté par Blackknight (site web personnel, Mastodon) . Évalué à 2.
Merci pour l'explication.
Il va vraiment falloir que je soigne mon aversion pour la syntaxe d'Ocaml parce que d'une part, tu as fait l'effort d'installer Gnat donc je peux bien faire l'effort aussi et d'autre part, j'ai vraiment l'impression de passer à coté de tout un pan de l'informatique en négligeant la partie fonctionnelle :D
Ou alors, je suis finalement trop vieux et je ne peux plus m'adapter ;)
C'est le côté autoritaire, limite autoritariste, du langage qui finit par prendre le dessus, on ne laisse pas d'accès à n'importe qui ;)
D'ailleurs je me rends compte que je fais la même chose en C/C++ et Java en collant des accolades partout pour limiter les portées :D
# Souvenirs souvenirs
Posté par gnumdk (site web personnel) . Évalué à 4.
J'ai commencé la programmation avec Ada 83 en DUT sur des VT200 avec vi sous HPUX.
Les heures à tenter de faire compiler ton programme codé sous Windows 98 + GNAT/Ada 95 sur Ada 83 :-)
# Fini !!
Posté par Blackknight (site web personnel, Mastodon) . Évalué à 3.
Finalement, j'ai pris la fin du programme à bras-le-corps en ajoutant l'internationalisation via ZanyBlue, chose que je n'avais jamais faite.
Comme je n'allais pas faire un deuxième journal pour ça, je préfère faire un commentaire ce qui permettra aux seuls intéressés d'être au courant :)
Comment ça marche
Comme souvent en Ada, on préfère la compilation à la gestion au runtime.
Du coup, le point d'entrée pour l'internationalisation, c'est le fichier de properties qui contient les clés à insérer dans les zones de texte.
Par exemple:
La syntaxe de ce fichier de propriétés est tout ce qu'il y a de plus standard pour qui en a déjà écrit en Java.
La seule chose vraiment notable vient de la ligne
Elle permet juste de préciser que l'on insérera une donnée dynamique, {0, mais qu'elle sera de type entière, integer}.
Pourquoi faire cela ? Car ZanyBlue vient avec un compilateur de fichier de propriétés qui permet de générer du code Ada et donc de typer les différents accesseurs.
Cela permet donc d'appeler notre substitution via, par exemple dans options.adb,
ou pour la précision
Le code généré fournit donc des fonctions permettant de formater une chaine à partir d'une clé mais elle fournit aussi des procédures se substituant aux entrées-sorties classiques d'Ada.
Ainsi, l'affichage du premier et du dernier message s'appellent tout simplement via
Ces appels possèdent un paramètre optionnel With_NL permettant de préciser si l'on doit retourner à la ligne ensuite.
Voilà, au final, c'était quasiment bête comme chou ;)
Comme d'habitude, c'est là
[^] # Re: Fini !!
Posté par mzf (site web personnel) . Évalué à 3.
Bravo pour avoir été jusqu'au bout du portage :-)
Est-ce que cela signifie qu'il faut recompiler le programme pour avoir une autre langue ?
[^] # Re: Fini !!
Posté par Blackknight (site web personnel, Mastodon) . Évalué à 2.
Merci.
C'était important pour moi d'aller au bout car cela m'a forcé à apprendre à utiliser de nouvelles bibliothèques.
Je ne suis pas encore un expert de ZanyBlue mais tel que je l'ai utilisé, les données texte sont embarquées dans le programme.
Du coup, oui, il faut recompiler le programme pour avoir une nouvelle langue. Mais comme les interfaces ne changent pas et que gprbuild gère ça assez correctement, je pense que cela ne recompilera pas grand chose.
Sinon, si je trouve un autre moyen plus dynamique, je le noterai ici bien que ce journal finisse par disparaître dans les flots des suivants ;)
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.