Langages impératifs et fonctionnels

James Roper a publié sur son blog un billet qui compare les performances de Java et de Scala via une réalisation du tri rapide d’une liste ou d’un tableau (quicksort). J’ai trouvé l’article intéressant pour deux raisons principales et qui sont liées à la nature particulière de ces deux langages.

Le premier aspect que j’ai trouvé intéressant dans le billet de Jon Roper est l’utilisation de la récursivité dans sa réalisation du tri rapide en Java. En effet, dans les langages dits impératifs, comme Java, l’accent est mis avant tout sur la structuration des données et sur le séquencement des instructions, souvent de niveau bas ou intermédiaire. Pour cette raison, dans ces langages, la récursivité est considérée comme un simple appel à la même opération et rien n’a été fait jusqu’à présent pour optimiser ces exécutions (quoique ce ne soit plus tout à fait vrai). C’est aussi pourquoi les développeurs codant avec de tels langages évitent (évitaient ?) dans la mesure du possible les appels récursifs ; d’autant plus que, non contrôlées, elles peuvent aboutir à un dépassement de pile !

Or, à la lumière de ceci, il est remarquable de constater que lorsque vous recherchez sur le Web des implémentations du tri rapide avec un langage impératif, la majorité qui ressortent reposent sur la récursivité. Et pourtant un tri rapide sans récursivité existe ; il suffit de jeter un œil sur sa réalisation dans les bibliothèques standards de certains langages (comme la bibliothèque GNU du C par exemple).

Maintenant, penchons nous du côté des langages fonctionnels. Ceux-ci mettent l’accent avant tout sur la structuration des opérations qui sont des fonctions très souvent de haut niveau. Cette structuration prend la forme de compositions de fonctions : une fonction est un graphe de fonctions dans lequel les nœuds représentent des combinateurs. Avec un tel schéma, la récursivité est naturelle et, aussi, depuis plus d’une décennie la majorité des langages fonctionnels ont optimisés celle-ci par ce que l’on appelle le tail recursivity (en gros, remplacement de la pile courante par celle de la fonction appelée).

Donc, la récursivité fait partie intégrante des langages fonctionnels et, à cet égard, il est fréquent d’entendre les tenants de ces langages que l’humain pense “naturellement” par récursivité, voir même par enchaînement de choses à faire :

le lave-vaisselle est mis en marche
    lorsque celui-ci est plein
        suite à un remplissage successif de vaisselles sales
            en provenance des différents repas
                s'étalant sur plusieurs jours (ouf !).

Ce à quoi, les tenants des langages impératifs sourient, haussent les épaulent, et rétorquent qu’au contraire l’humain pense séquentiellement :

* à chaque fin de repas, la vaisselle sale est mise dans le lave-vaisselle,
* si le lave-vaisselle est plein alors il est mis en marche,
* etc.

Un autre exemple qui illustre bien cet aspect séquentiel est évidemment la recette de cuisine.

Dans ce cas, pourquoi ces développeurs utilisent-ils alors quasi-naturellement la récursivité ou des compositions de fonctions dans certains problèmes (comme ici le tri rapide) en lieu et place de boucles, d’accumulateurs, de code de contrôle, de sauts, etc ? Finalement qui a raison ? Ma réponse est : les deux. Et c’est pourquoi il existe des patterns aux couleurs fonctionnelles dans les langages impératifs (visiteurs, chaînes de responsabilité, etc.) et que la majorité des langages fonctionnels ont implémentés le séquencement (la monade IO par exemple dans Haskell). Néanmoins, dans la plupart des problèmes que j’ai rencontré, l’approche fonctionnelle apporte souvent une solution plus simple, plus élégante ; mais elle demande, parfois, en contre partie, de penser d’une façon qui, au premier abord, ne parait pas toujours aisée.

Le second aspect intéressant de l’article de James Roper est que les tests de performances présentés dans l’article n’avaient pas pour objectif finalement de montrer lequel des deux langages, Java ou Scala, est le plus performant. D’ailleurs sur ce sujet, les résultats sont mitigés : avec une approche plus “impérative” Scala présente des performances bien meilleures qu’avec une approche fonctionnelle. Non, le sujet de l’article est que l’on a que faire finalement des performances brutes dans une application de type serveur le plus souvent complexe. La problématique se porte ailleurs, et ceci d’autant plus avec les architectures matérielles multi-cœurs. Elle repose désormais sur la capacité de l’application à monter en charge et, pour ce faire, comment profiter au mieux des processeurs multi-cœurs. Et une application écrite dans un langage à la syntaxe hétéroclite, à l’exécution lente, mais qui profite au mieux des architectures actuelles, peut au contraire bien mieux monter en charge que celle réalisée dans un langage naturellement très performant mais plus bas niveau ; il suffit pour cela de jeter un œil du côté du langage Erlang et de la plate-forme OTP sur laquelle il repose. Toutefois, là où je suis sceptique avec sa conclusion, est l’aptitude de Scala de permettre aux applications de bien monter en charge. En effet, si, par son approche fonctionnelle et par la disponibilité de bibliothèques dans ce domaine, la prise en compte de cette problématique sera plus aisée qu’avec des langages impératifs, les caractéristiques de la montée en charge et surtout de la haute disponibilité sont des aspects avant tout techniques qui dépendent principalement du runtime. Et, sous cet angle, le langage pourra apporter tout ce qu’il voudra sur ces créneaux, l’application restera contrainte par le runtime sous-jacent. Et c’est pourquoi, aujourd’hui, une application sur une VM Erlang montera plus facilement en charge et profitera bien mieux des capacités multi-cœurs des architectures actuelles que sur une VM Java. Et c’est aussi la raison pour laquelle il faudra encore aujourd’hui des outils tiers (terracotta, infinispan, …) pour permettre aux applications sur JVM de mieux répondre à cette problématique ; là où une seule VM Erlang suffit, il faudra aligner plusieurs VM Java.

Miguel Moquillon

Author: Miguel Moquillon

Restez au courant de l'actualité et abonnez-vous au Flux RSS de cette catégorie

Commentaires (0)

Les commentaires sont fermés


Aucune annexe



À voir également

Evolution de code avec Haskell (partie 2) : évolution

Un logiciel n’est jamais terminé. Il ne fini pas d’évoluer pour satisfaire aussi bien de nouveaux besoins que de nouveaux enjeux technologiques. Un logiciel qui ne change pas, qui ne vit pas un refactoring continue, est un logiciel qui se meurt jusqu’à disparaître du marché parce que dépassé. Nous savons faire évoluer une application écrite selon la POO en jouant sur les propriétés de rétention, de composition, et d’extension des objets, ces entités logicielles qui représentent les concepts adressés par le programme. Mais qu’en est-il en programmation fonctionnel ? Comment peuvent être représentés les concepts ? Comment un code, écrit avec un langage fonctionnel, peut-il évoluer face aux changements ? Je vous propose de montrer ces aspects par un petit tour d’horizon d’un programme écrit en Haskell.

Lire la suite

Evolution de code avec Haskell (partie 1) : conceptualisation

Un logiciel n’est jamais terminé. Il ne fini pas d’évoluer pour satisfaire aussi bien de nouveaux besoins que de nouveaux enjeux technologiques. Un logiciel qui ne change pas, qui ne vit pas un refactoring continue, est un logiciel qui se meurt jusqu’à disparaître du marché parce que dépassé. Nous savons faire évoluer une application écrite selon la POO en jouant sur les propriétés de rétention, de composition, et d’extension des objets, ces entités logicielles qui représentent les concepts adressés par le programme. Mais qu’en est-il en programmation fonctionnel ? Comment peuvent être représentés les concepts ? Comment un code, écrit avec un langage fonctionnel, peut-il évoluer face aux changements ? Je vous propose de montrer ces aspects par un petit tour d’horizon d’un programme écrit en Haskell.

Lire la suite