Comme tu es plus habitué au paradigme impératif, je donne ci-dessous la boucle while équivalente à mon code avec une fonction récursive, puis je donne une explication pour passer de l'un à l'autre.
(* valeurs impératives modifiées pendant la boucle, on les définit à l'éxtérieur avant de rentrer dedans*)letsamples=Sample.createsample_sizeinletstamp=ref(Mtime_clock.now())inletkey_pressed=ref(tryinput_charstdinwithEnd_of_file->'q')in(* on rentre dans la boucle *)while!key_pressed<>'q'do(* on ne fait quelque chose qu'en cas d'appui sur 'enter' *)if!key_pressed='\n'thenbeginletnew_stamp=Mtime_clock.now()inletelapsed=Mtime.(Span.to_s(span!stampnew_stamp))in(* logique de mise à jour de la variable `samples` *)ifelapsed>reset_delaythenSample.resetsamples;Sample.addnew_stampsamples;(* on affiche le message adapté *)show_bpmsamples;(* mise à jour de la variable `stamp` *)stamp:=new_stamp;end;(* mise à jour de la variable `key_pressed` *)key_pressed:=tryinput_charstdinwithEnd_of_file->'q'done;
Le principe d'une boucle while, comme illustré dans cet exemple, est d'être une procédure qui modifie par effet de bords certaines variables qui lui sont globales (ici essentiellement samples et stamp) tant qu'une condition est satisfaite (ici, tant que l'on n'a pas appuyé sur q ou que l'entrée standard est n'est pas au bout). En tant que telle, du point de vue d'OCaml, c'est une expression qui a une valeur (comme n'importe quelle expression du langage) de type unit : ce type ne contient qu'une seule valeur (on parle de type singleton) notée (), c'est la valeur qui ne contient aucune information.
Pour transformer cette boucle en une fonction récursive, il faut d'abord qu'elle retourne la même valeur, à savoir (), lorsqu'elle termine. Ensuite, les variables globales sur laquelle opérait la boucle sont transformées en paramètres de la fonction : elles ne seront plus globales à la boucle, mais locales. On commence donc par écrire :
letrecloopstampsamples=
Ensuite, il faut exprimer la condition d'arrêt de la boucle. Celle-ci dépend du caractère que l'on lit sur l'entrée standard, d'où :
letrecloopstampsamples=matchinput_charstdinwith
On fait alors une étude de cas, à la manière d'un switch, en commençant par dire quand la fonction termine (et qui renvoie alors ()) :
letrecloopstampsamples=matchinput_charstdinwith(* cas de fin de boucle *)|exceptionEnd_of_file|'q'->()
Vient ensuite le cœur de la logique de la boucle, ce qui se passe quand on a appuyé sur enter :
letrecloopstampsamples=matchinput_charstdinwith(* cas de fin de boucle *)|exceptionEnd_of_file|'q'->()(* on a pressé 'enter' *)|'\n'->(* on remet la même logique qu'avec la boucle while *)letnew_stamp=Mtime_clock.now()inletelapsed=Mtime.(Span.to_s(span!stampnew_stamp))in(* logique de mise à jour de la variable `samples` *)ifelapsed>reset_delaythenSample.resetsamples;Sample.addnew_stampsamples;(* on affiche le message adapté *)show_bpmsamples;(* ici on ne met pas à jour la variable stamp mais on relance la boucle avec les nouveux paramètres *)loopnew_stampsamples
Il reste enfin à traiter le cas où l'on a appuyé sur une autre touche : on ne fait rien et on relance la boucle avec les mêmes paramètres
letrecloopstampsamples=matchinput_charstdinwith(* je ne réécris pas la gestion des autres cas *)(* cas par défaut : on boucle sans rien changer *)|_->loopstampsamples
Maintenant que le corps de la fonction est écrit, il ne reste plus qu'à l'appeler pour lancer la boucle. Pour ce faire, on appelle la fonction avec, pour paramètres, les valeurs initiales des variables globales de la boucle while :
letrecloopstampsamples=matchinput_charstdinwith(* bla bla bla bla*)inloop(Mtime_clock.now())(Sample.createsample_size)
Voilà le principe général pour transformer une boucle while en fonction récursive : on transforme les variables globales de la boucle en variables locales, et à chaque tour on lui passe les nouvelles valeurs.
Ceci étant, ce genre d'approche n'est pas propre au paradigme fonctionnel, mais ce dernier en fait un usage omniprésent et c'est la route vers la pureté (absence d'effets de bords)1. Pour l'instant, la variable samples fonctionne toujours pas effet de bords et, dans le message précédent, j'ai juste modifié l'écriture de sa logique de mise à jour en utilisant une approche par pipe avec le code :
Mais si, à la place d'une structure impérative, j'utilisais une structure purement applicative, j'aurais juste à changer les deux premiers combinateurs de tuyaux : un pipe |> au lieu d'un T |- et utiliser la fonction identity au lieu de ignore
samples|>(ifelapsed>reset_delaythenSample.resetelseidentity)|>Sample.addnew_stamp|-show_bpm(* ici on log donc on utilise toujours le tee *)|>loopnew_stamp
Derrière cette vision par pipeline, il y a une notion élémentaire de mathématique (bon c'est pas du niveau primaire, mais début de lycée ;-), à savoir la composition de fonction.
composition
que l'on peut écrire (la pipeline est assez visible sur le diagramme) :
funx->x|>f|>g|>h
L'opérateur de composition est une opérateur d'ordre supérieur : il prend deux fonctions en entrée et en renvoie une en sortie; raison pour laquelle il a une place centrale de le paradigme de la programmation fonctionnelle. Que peux-t-on faire avec une fonction ?
les utiliser, ça on le fait dans tous les langages de la même façon ;
les composer, ça c'est plus simple à écrire dans un langage fonctionnel.
J'espère que ces explications t'aideront un peu mieux à comprendre certains principes élémentaires à la base du paradigme fonctionnel.
En réalité, c'est cette recherche d'absence d'effets de bord qui nous les fait écrire ainsi. On obtient alors des fonctions récursives avec appel dits terminaux, que le compilateur optimisera comme une simple boucle (il fera la traduction dans le sens inverse de celle que je viens de faire). Voir mon commentaire sur la version de taptempo en Emacs Lisp. ↩
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
[^] # Re: Lisibilité
Posté par kantien . En réponse au journal Portage de TapTempo en OCaml. Évalué à 5. Dernière modification le 13 mars 2018 à 17:49.
Comme tu es plus habitué au paradigme impératif, je donne ci-dessous la boucle
whileéquivalente à mon code avec une fonction récursive, puis je donne une explication pour passer de l'un à l'autre.Le principe d'une boucle
while, comme illustré dans cet exemple, est d'être une procédure qui modifie par effet de bords certaines variables qui lui sont globales (ici essentiellementsamplesetstamp) tant qu'une condition est satisfaite (ici, tant que l'on n'a pas appuyé surqou que l'entrée standard est n'est pas au bout). En tant que telle, du point de vue d'OCaml, c'est une expression qui a une valeur (comme n'importe quelle expression du langage) de typeunit: ce type ne contient qu'une seule valeur (on parle de type singleton) notée(), c'est la valeur qui ne contient aucune information.Pour transformer cette boucle en une fonction récursive, il faut d'abord qu'elle retourne la même valeur, à savoir
(), lorsqu'elle termine. Ensuite, les variables globales sur laquelle opérait la boucle sont transformées en paramètres de la fonction : elles ne seront plus globales à la boucle, mais locales. On commence donc par écrire :Ensuite, il faut exprimer la condition d'arrêt de la boucle. Celle-ci dépend du caractère que l'on lit sur l'entrée standard, d'où :
On fait alors une étude de cas, à la manière d'un switch, en commençant par dire quand la fonction termine (et qui renvoie alors
()) :Vient ensuite le cœur de la logique de la boucle, ce qui se passe quand on a appuyé sur
enter:Il reste enfin à traiter le cas où l'on a appuyé sur une autre touche : on ne fait rien et on relance la boucle avec les mêmes paramètres
Maintenant que le corps de la fonction est écrit, il ne reste plus qu'à l'appeler pour lancer la boucle. Pour ce faire, on appelle la fonction avec, pour paramètres, les valeurs initiales des variables globales de la boucle
while:Voilà le principe général pour transformer une boucle
whileen fonction récursive : on transforme les variables globales de la boucle en variables locales, et à chaque tour on lui passe les nouvelles valeurs.Ceci étant, ce genre d'approche n'est pas propre au paradigme fonctionnel, mais ce dernier en fait un usage omniprésent et c'est la route vers la pureté (absence d'effets de bords)1 . Pour l'instant, la variable
samplesfonctionne toujours pas effet de bords et, dans le message précédent, j'ai juste modifié l'écriture de sa logique de mise à jour en utilisant une approche par pipe avec le code :Mais si, à la place d'une structure impérative, j'utilisais une structure purement applicative, j'aurais juste à changer les deux premiers combinateurs de tuyaux : un pipe
|>au lieu d'un T|-et utiliser la fonctionidentityau lieu deignoreDerrière cette vision par pipeline, il y a une notion élémentaire de mathématique (bon c'est pas du niveau primaire, mais début de lycée ;-), à savoir la composition de fonction.
composition
que l'on peut écrire (la pipeline est assez visible sur le diagramme) :
L'opérateur de composition est une opérateur d'ordre supérieur : il prend deux fonctions en entrée et en renvoie une en sortie; raison pour laquelle il a une place centrale de le paradigme de la programmation fonctionnelle. Que peux-t-on faire avec une fonction ?
J'espère que ces explications t'aideront un peu mieux à comprendre certains principes élémentaires à la base du paradigme fonctionnel.
En réalité, c'est cette recherche d'absence d'effets de bord qui nous les fait écrire ainsi. On obtient alors des fonctions récursives avec appel dits terminaux, que le compilateur optimisera comme une simple boucle (il fera la traduction dans le sens inverse de celle que je viens de faire). Voir mon commentaire sur la version de taptempo en Emacs Lisp. ↩
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.