Journal
Ce journal contient 3 entrées.
Java 8 Stream Tutorial - Benjamin Winterberg
Un gros tutoriel par l'exemple pour l'API Stream de Java 8. Dans cette API, un stream (flux) est une suite d'éléments sur laquelle des opérations successives vont être appliquées (map, reduce, filtre, tri, agrégation, etc.). À noter que les traitements sur le flux peuvent être parallélisées très facilement.
Le code est particulièrement élégant, du fait de l'approche fonctionnelle de l'API (https://fr.wikipedia.org/wiki/Programmation_fonctionnelle). Cela rappelle, par certains côtés, l'API LINQ de C#.
J'apprécie beaucoup que Java se dote de plus en plus de sucre syntaxique : les lambdas à la place des classes anonymes, les try with resources à la place des try/finally, les foreach à la place des itérateurs, les imports statiques, etc.
Au fil des évolutions, la critique comme quoi Java serait "trop objet", déjà plutôt idiote à l'origine*, devient parfaitement caduque. En effet Java peut être vu comme un langage multiparadigme :
- impératif, si l'on utilise des méthodes et des imports statiques
- objet, évidemment
- fonctionnel (à l'origine avec les classes anonymes et du bidouillage), grâce aux lambdas
Bien sûr, avec les librairies appropriées, d'autres approches sont possibles : programmation orientée aspects, programmation réactive, etc.
* Plutôt idiote car, indépendamment de tout langage, un paradigme de programmation est une manière d'écrire et de structurer des programmes. Un langage exploite les philosophies d'un ou de plusieurs paradigmes pour fournir des constructions syntaxiques qui simplifient l'écriture et la lecture du code pour un programmeur ayant décidé de penser conformément à ces philosophies. Rien n'empêche d'exploiter un langage et d'y écrire des programmes en se conformant à une philosophie différente. Le C est un exemple flagrant : impératif par nature, il est tellement permissif, que l'on peut y penser en objet, en fonctionnel, en réactif, etc.

Le code est particulièrement élégant, du fait de l'approche fonctionnelle de l'API (https://fr.wikipedia.org/wiki/Programmation_fonctionnelle). Cela rappelle, par certains côtés, l'API LINQ de C#.
J'apprécie beaucoup que Java se dote de plus en plus de sucre syntaxique : les lambdas à la place des classes anonymes, les try with resources à la place des try/finally, les foreach à la place des itérateurs, les imports statiques, etc.
Au fil des évolutions, la critique comme quoi Java serait "trop objet", déjà plutôt idiote à l'origine*, devient parfaitement caduque. En effet Java peut être vu comme un langage multiparadigme :
- impératif, si l'on utilise des méthodes et des imports statiques
- objet, évidemment
- fonctionnel (à l'origine avec les classes anonymes et du bidouillage), grâce aux lambdas
Bien sûr, avec les librairies appropriées, d'autres approches sont possibles : programmation orientée aspects, programmation réactive, etc.
* Plutôt idiote car, indépendamment de tout langage, un paradigme de programmation est une manière d'écrire et de structurer des programmes. Un langage exploite les philosophies d'un ou de plusieurs paradigmes pour fournir des constructions syntaxiques qui simplifient l'écriture et la lecture du code pour un programmeur ayant décidé de penser conformément à ces philosophies. Rien n'empêche d'exploiter un langage et d'y écrire des programmes en se conformant à une philosophie différente. Le C est un exemple flagrant : impératif par nature, il est tellement permissif, que l'on peut y penser en objet, en fonctionnel, en réactif, etc.
C'est quoi la récursion terminale ?
Avant toute chose, nous devons parler de la pile d'exécution (ou "pile des appels", ou tout simplement "pile").
La pile d'exécution est un espace de stockage permettant d'enregistrer l'endroit où chaque fonction active (= les fonctions invoquées mais non terminées) doit retourner à la fin de son exécution. Elle enregistre donc des adresses, mais aussi le contexte des fonctions actives (les variables locales, les paramètres, etc.).
Le principe de la pile est le suivant: si une fonction A invoque une fonction B, alors on stocke A au sommet de la pile pour savoir où reprendre lorsque B sera terminée. Si B invoque une fonction C, B sera placé au sommet de la pile à son tour. A la fin de C, on récupère B au sommet de la pile, et à la fin de B on récupère A au sommet de la pile (d'où le terme de pile, car on y empile et on y dépile des éléments).
Alors, imaginons une factorielle naïve :
fact(n)
{
if(n == 1) // cas de base
return 1;
else
return n * fact(n - 1);
}
Décomposons notre fonction fact : pour calculer n * fact(n - 1), il est évidemment nécessaire de calculer fact(n - 1). Cela implique que fact(n) ne peut pas retourner de résultat sans d'abord obtenir le résultat de fact(n - 1). Avant d'exécuter fact(n - 1), on place donc fact(n) au sommet de la pile et on exécute fact(n - 1).
Sauf qu'évidemment, le problème se pose à nouveau : pour calculer fact(n - 1) on doit tout d'abord calculer fact(n - 2), et on place donc fact(n - 1) sur la pile. Et ainsi de suite jusqu'à ce que l'on atteigne fact(1) qui retourne immédiatement 1.
Le souci c'est que notre pile n'est pas infinie. Dans les langages classiques type C ou Java, elle est même ridiculement petite. Lorsqu'elle est pleine, le programme crashe car il n'est plus possible d'invoquer de nouvelle fonction : c'est le dépassement (ou débordement) de pile, stack overflow en anglais.
Une fonction récursive terminale, c'est tout simplement une fonction récursive particulière, où la récursion ne fait intervenir aucune autre opération que l'appel de la fonction. Ce qui, pour notre factorielle se traduit ainsi :
fact(n, total)
{
if(n == 0)
return total;
else
return fact(n - 1, total * (n - 1));
}
Ici, lorsque l'on arrive fact(n - 1, total * (n - 1)), il n'y a rien à sauvegarder. Il suffit donc simplement de remplacer fact(n, total) par fact(n - 1, total * (n - 1)) au sommet de la pile. L'espace nécessaire sur la pile pour exécuter fact(n, total) est donc constant, ce qui assure que la pile ne débordera pas, même pour un très grand nombre de récursion.
Hélas, cette optimisation n'est pas implicite et doit être effectuée à la compilation. Beaucoup de langages classiques ne le font pas (Java, C#, Python, C/C++ sauf avec l'option -foptimize-sibling-calls, etc.). Il s'agit donc principalement d'une propriété typique des langages fonctionnels car ceux ci font massivement appel à la récursion.
La pile d'exécution est un espace de stockage permettant d'enregistrer l'endroit où chaque fonction active (= les fonctions invoquées mais non terminées) doit retourner à la fin de son exécution. Elle enregistre donc des adresses, mais aussi le contexte des fonctions actives (les variables locales, les paramètres, etc.).
Le principe de la pile est le suivant: si une fonction A invoque une fonction B, alors on stocke A au sommet de la pile pour savoir où reprendre lorsque B sera terminée. Si B invoque une fonction C, B sera placé au sommet de la pile à son tour. A la fin de C, on récupère B au sommet de la pile, et à la fin de B on récupère A au sommet de la pile (d'où le terme de pile, car on y empile et on y dépile des éléments).
Alors, imaginons une factorielle naïve :
fact(n)
{
if(n == 1) // cas de base
return 1;
else
return n * fact(n - 1);
}
Décomposons notre fonction fact : pour calculer n * fact(n - 1), il est évidemment nécessaire de calculer fact(n - 1). Cela implique que fact(n) ne peut pas retourner de résultat sans d'abord obtenir le résultat de fact(n - 1). Avant d'exécuter fact(n - 1), on place donc fact(n) au sommet de la pile et on exécute fact(n - 1).
Sauf qu'évidemment, le problème se pose à nouveau : pour calculer fact(n - 1) on doit tout d'abord calculer fact(n - 2), et on place donc fact(n - 1) sur la pile. Et ainsi de suite jusqu'à ce que l'on atteigne fact(1) qui retourne immédiatement 1.
Le souci c'est que notre pile n'est pas infinie. Dans les langages classiques type C ou Java, elle est même ridiculement petite. Lorsqu'elle est pleine, le programme crashe car il n'est plus possible d'invoquer de nouvelle fonction : c'est le dépassement (ou débordement) de pile, stack overflow en anglais.
Une fonction récursive terminale, c'est tout simplement une fonction récursive particulière, où la récursion ne fait intervenir aucune autre opération que l'appel de la fonction. Ce qui, pour notre factorielle se traduit ainsi :
fact(n, total)
{
if(n == 0)
return total;
else
return fact(n - 1, total * (n - 1));
}
Ici, lorsque l'on arrive fact(n - 1, total * (n - 1)), il n'y a rien à sauvegarder. Il suffit donc simplement de remplacer fact(n, total) par fact(n - 1, total * (n - 1)) au sommet de la pile. L'espace nécessaire sur la pile pour exécuter fact(n, total) est donc constant, ce qui assure que la pile ne débordera pas, même pour un très grand nombre de récursion.
Hélas, cette optimisation n'est pas implicite et doit être effectuée à la compilation. Beaucoup de langages classiques ne le font pas (Java, C#, Python, C/C++ sauf avec l'option -foptimize-sibling-calls, etc.). Il s'agit donc principalement d'une propriété typique des langages fonctionnels car ceux ci font massivement appel à la récursion.
Functors, Applicatives, And Monads In Pictures
La programmation fonctionnelle emprunte un vocabulaire parfois obscur aux mathématiques, d'où cet intéressant petit article illustré qui revient sur les concepts de "fonctor" (foncteur), "monad" (monoïde) et "applicative" (foncteur applicatif).
Une autre explication, en français :
http://lyah.haskell.fr/foncteurs-foncteurs-applicatifs-et-monoides

Une autre explication, en français :
http://lyah.haskell.fr/foncteurs-foncteurs-applicatifs-et-monoides
Ce journal est basé sur Ginger, un gestionnaire de lien minimaliste développé dans le cadre d'un stage de perfectionnement. Pour plus d'informations, consulter le wiki consacré à mes projets personnels.