points de vue - Mot-clé - programmation impérativeles déambulations d'un codeur2023-06-18T04:35:57+02:00Miguel Moquillonurn:md5:2d53372d0fa8f28ff50e71462648e0b8DotclearL'extensibilité d'objets métiers dans différents langagesurn:md5:d9f16855bbdf5f44d33ab8ef1572ced22018-04-27T21:29:00+02:002022-08-19T14:01:06+02:00Miguel MoquillonTechniques de programmationHaskellJavaprogrammation fonctionnelleprogrammation impérativeprogrammation orientée objetSmalltalk<p>Il arrive fréquemment qu’avec l’évolution des besoins dans le temps, les objets métiers qui ont été définis auparavant nécessitent d’être étendu par l’ajout de nouvelles fonctionnalités. Selon la nature des langages de programmation, mais aussi selon les caractéristiques propres aux langages, les méthodes d’extensions varient et peuvent être plus ou moins aisées à mettre en œuvre, en particulier lorsque les extensions sont fournies dans des modules (paquetages, bibliothèques, …) à part et que l’existant ne doit pas être impacté par ces ajouts (ou du moins le minimum possible).</p>
<p>Dans ce petit billet je voudrais vous présenter certaines d’entre elles, et en particulier dans le contexte présenté ci-dessus, et ceci avec trois langages de programmations différents : Java, un langage impératif (orienté classe), Smalltalk, un des rares langages qui soient vraiment orienté objet, et Haskell, un langage fonctionnel.</p> <p>Pour illustrer les différentes techniques d’extensibilité, je vous propose un exemple simple de la gestion d’utilisateurs. Celui-ci s’articule autour du type <code>User</code> et des opérations classiques liées à son profil et à son authentification. Par exemple, en Java :</p>
<pre>
<code class="language-java">public class User {
private Identifier id;
private final String firstName;
private String lastName;
private Credentials credentials = null;
protected User() {
// for the persistence engine
}
public User(final String firstName, final String lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.credentials = Credentials.none();
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
public String getFullName() {
return this.firstName + ' ' + this.lastName;
}
public void setLastName(final String newLastName) {
this.lastName = newLastName
}
public void setCredentials(final Credentials newCredentials) {
this.credentials = newCredentials.copy();
}
public void authenticate(final Credentials auth) throws AuthenticationException {
this.credentials.check(auth);
}
}</code></pre>
<p>Nous omettons dans notre exemple, pour plus de clarté, tout ce qui est en rapport avec la persistance.</p>
<p>Smalltalk est plus qu’un langage. C’est avant tout un environnement de développement et d’exécution interactif virtuel complet. La programmation se fait essentiellement via ses outils (en l’occurrence le navigateur de classes et le débogueur) et celle-ci n’est pas structurée autour de fichiers comme avec les autres langages. Tout est maintenu dans une image de machine virtuelle (à l’image des VM VMWare ou VirtualBox) . Aussi, pour plus clarté, dans ce qui suit, j’adopte la notation en vigueur de la documentation sur ce langage :</p>
<pre>
<code class="language-smalltalk">Object subclasses: #User
instanceVariableNames:'firstName lastName credentials'
classVariableNames:''
poolDictionaries:''
User >> initializeFirstName: aFirstName lastName: aLastName
firstName := aFirstName.
lastName := aLastName
User class >> firstName: aFirstName lastName: aLastName
|user|
user := self new initializeFirstName: aFirstName lastName: aLastName.
^ user
User >> lastName: newLastName
lastName := newLastName
User >> lastName
^ lastName
User >> fullName
^ self firstName, ' ', self lastName
User >> credentials: newCredentials
credentials := newCredentials
User >> authenticateWith: inputCredentials
credentials check: inputCredentials </code></pre>
<p>Et pour finir en Haskell :</p>
<pre>
<code class="language-haskell">data User = User { firstName :: String,
lastName :: String,
credentials :: Credentials
} deriving (Show)
fullName u = firstName u ++ ' ' ++ lastName u
authenticate u c = checkCredentials (credentials u) c </code></pre>
<p>Quelque temps après, il est décidé de rajouter une fonctionnalité de messagerie instantanée et le choix a été de déléguer sa gestion à un service extérieur, l’application proposant juste le client. Pour ce faire, il est nécessaire de rajouter à l’utilisateur les informations relatives à la messagerie instantanée. Par exemple, avec un service XMPP, à minima le domaine et les crédences propres à la connexion au service Jabber distant. Avec des langages comme Java il existe différentes façon de faire :</p>
<ul>
<li>rajouter directement à la classe <code>User</code> les informations nécessaires,</li>
<li>étendre la classe User par une nouvelle, par exemple <code>ChatUser</code>,</li>
<li>décorer la classe <code>User</code> par une nouvelle classe ou mieux par un <em>trait</em> (un ensemble transverse de propriétés qui peut être tissé à des objets enfin de les enrichir en comportement) et qui serait propre au service de messagerie instantanée.</li>
</ul>
<p>Dans notre contexte, tout ce qui a trait aux utilisateurs est défini dans un module cœur de l’application et le service de messagerie instantanée est apporté par autre un module, optionnel. De plus on veut étendre le module cœur par les nouvelles fonctionnalités mais sans à entacher celui-ci. Au regard de ceci, la première solution est à éviter. Reste les deux dernières. La dernière solution est séduisante parce qu’elle permet à différents modules de décorer les objets métiers en fonctionnalités additionnelles sans que ceux-ci en aient connaissance. En Java, en général, une classe wrapper est écrite :</p>
<pre>
<code class="language-java">public class ChatUser extends User {
private User wrapped;
private Credentials chatCredentials;
private String chatDomain;
protected ChatUser() {
// for the persistence engine
}
protected void setDecoratedUser(final User user) {
this.wrapped = user;
}
public static ChatUser decorate(final User user) {
ChatUser chatUser = ChatUserRepository.getInstance().get(user.getId());
chatUser.setDecoratedUser(user);
return chatUser;
}
public Credentials getChatCredentials() {
return this.chatCredentials;
}
public void setChatCredentials(final Credentials credentials) {
this.chatCredentials = credentials;
}
public String getChatDomain() {
return this.chatDomain;
}
public void setChatDomain(final String domain) {
this.chatDomain = domain;
}
...
}</code></pre>
<p>Dans notre exemple, les propriétés additionnelles à un utilisateur et relatives à la messagerie instantanée sont directement récupérées d’une source de données avant de décorer avec celles-ci un utilisateur de notre application. Les autres méthodes héritées de la classe <code>User</code> délèguent leurs appels aux méthodes correspondantes de l’objet encapsulé. Les IDE en général permettent de générer rapidement celles-ci. Comme les nouvelles propriétés ne seront utilisées que par le code du module responsable de la messagerie instantanée, la décoration ne se fera donc que par lui. Là où cette approche est plus délicate à mettre en œuvre est lorsqu’il y a plusieurs modules qui décorent chacun l’objet métier et que certaines décorations peuvent être transverses et utilisées conjointement par un même code :</p>
<pre>
<code class="language-java">User aUser = ...; // get in fact a ChatUser decorating the DelegationUser that wraps the User
String fullName = aUser.getFullName(); // ok
String chatDomain = ((ChatUser)aUser).getChatDomain(); // ok
User delegate = ((DeletgationUser)aUser).getDelegation().getDelegate(); // ooops aUser isn't a DelegationUser in first place and hence it doesn't understand this invocation!</code></pre>
<p>On peut s’en sortir par différentes approches, par exemple :</p>
<ul>
<li>le code utilisateur utilise <em>explicitement</em> chaque décoration de l’objet métier, par exemple en décorant à nouveau l’objet métier ;</li>
<li>on met en place de façon simplifiée un POM (Protocol Object Model) dans lequel on associe à chaque objet métier son modèle, un objet générique. Celui-ci encapsule en fait l’objet concret auquel il est associé et fait appel à la rétrospection pour requêter celui-ci. Avec cette approche, il suffit juste de demander au modèle, sous forme de chaîne de caractères en Java, telle propriété de décoration ; on passe donc d’une approche de programmation statique à une approche dynamique avec les problèmes que ça peut soulever dans un langage à typage statique.</li>
</ul>
<p>Avec Smalltalk, les choses sont beaucoup plus simples. La solution la plus directe est d’enrichir <em>indirectement</em> l’objet métier, chose que l’on peut faire aussi en Ruby, en Groovy, et même en Kotlin. Toutefois, en Smalltalk, non seulement chaque module peut enrichir les objets d’autres modules (et ceux du langage), mais on peut aussi savoir qui a rajouté quoi. Avec notre notation, ça donnerai ceci :</p>
<pre>
<code class="language-smalltalk">User addInstVarNamed: 'chatDomain chatCredentials'.
User >> chatDomain
^ chatDomain
User >> chatDomain: domain
chatDomain := domain
User >> chatCredentials
^ chatCredentials
User >> chatCredentials: newCredentials
chatCredentials := newCredentials
</code></pre>
<p>Cette solution a l’avantage de répondre simplement au cas de décorations transverses et utilisées conjointement par un même code. Une autre solution serait, par ce même mécanisme, d’ajouter comme variable d’instance dans la classe <code>User</code> l’objet qui rassemble les propriétés à enrichir, par exemple un objet <code>ChatProfile</code>, et d’utiliser le mécanisme interne de délégation de Smalltalk pour rediriger les envois de messages non compris par <code>User</code> à ses délégués (en l’occurrence <code>ChatProfile</code>) ; ceci se fait simplement en surchargeant la méthode <code>doesNotUnderstand:</code> dont un exemple est donné ci-dessous :</p>
<pre>
<code class="language-smalltalk">User >> doesNotUnderstand: aMessage
|return|
return := aMessage sendTo: chatProfile.
^ (return class = ChatProfile)
ifTrue: [ ^ self ]
ifFalse: [ ^ return ]
</code></pre>
<p>Évidemment, nous pouvons rendre le code plus générique en définissant, en lieu et place d’une variable d’instance <code>chatProfile</code>, une liste de délégués dans laquelle est rajouté par le module de la messagerie instantanée une instance de <code>ChatProfile</code>. Il suffit alors dans l’implémentation de <code>doesNotUnderstand:</code> de parcourir l’ensemble des délégués pour vérifier si l’un d’eux sache répondre au message reçu en vue de le lui envoyer. Avec cette approche, on rend l’objet métier <code>User</code> extensible en permettant à chaque module de pouvoir ajouter aisément un décorateur.</p>
<p>Pour finir, voyons comment faire en Haskell. La solution la plus simple est de définir directement les fonctions qui permettent d’obtenir les informations additionnelles à un utilisateur dans le module correspondant à la nouvelle fonctionnalité. En effet, dans l’approche fonctionnelle, les fonctions sont les briques de base de la composition. Par exemple, de façon simplifiée :</p>
<pre>
<code class="language-haskell">module MyApp.Chat(chatCredentials, chatDomain) where
import MyApp.Core.User
import MyApp.Core.Security
...
data ChatInfo = { domain :: String,
credentials :: Credentials }
deriving (Show, Read)
loadChatInfo u = do
...
chatCredentials user = do
let c <- loadChatInfo user
return credentials c
chatDomain user = do
let c <- loadChatInfo user
return domain c
</code></pre>
<p>Toutefois l’approche la plus correcte et la plus extensible serait de profiter des types algébriques pour définir l’ensemble des propriétés qui sont propre d’une part au profil des utilisateurs de l’application, et d’autre part qui sont propre au profil des utilisateurs de la messagerie instantanée, sachant qu’il existe un lien entre un compte utilisateur et celui de la messagerie instantanée. Une solution pourrait être celle-ci :</p>
<pre>
<code class="language-haskell">module MyApp.Core.User (
Profile(..),
User(..)
) where
import MyApp.Core.Security
...
class Profile a where
firstName :: a -> String
lastName :: a -> String
fullName :: a -> String
credentials :: a -> Credentials
data User = User String String Credentials deriving (Show, Read)
instance Profile User where
firstName (User f _ _) = f
lastName (User _ l _) = l
fullName (User f l _) = f ++ " " ++ l
credentials (User _ _ c) = c
</code></pre>
<p>et</p>
<pre>
<code class="language-haskell">module MyApp.Chat (
ChatUser(..),
ChatProfile(..)
) where
import MyApp.Core.User
import MyApp.Core.Security
...
class Profile c => ChatProfile c where
chatDomain :: c -> String
chatCredentials :: c -> Credentials
data ChatUser u = ChatUser u String Credentials deriving (Show, Read)
instance Profile u => Profile (ChatUser u) where
firstName (ChatUser u _ _) = firstName u
lastName (ChatUser u _ _) = lastName u
fullName (ChatUser u _ _) = fullName u
credentials (ChatUser u _ _) = credentials u
instance Profile u => ChatProfile (ChatUser u) where
chatDomain (ChatUser _ domain _) = domain
chatCredentials (ChatUser _ _ credentials) = credentials
</code></pre>
<p>Ceci nécessite évidemment de devoir réécrire le module des utilisateurs si cette approche n’a pas déjà été adoptée. Néanmoins, ensuite, les profils utilisateurs pourront être enrichis par des modules extérieurs sans nécessiter de modification des modules cœurs. De plus, en général, dans une application en Haskell, l’approche de modélisation des fonctions métiers par les types algébriques est couramment utilisée.</p>
<p>Ce petit billet est terminé et j’espère que vous aurez apprécié celui-ci. Ce dernier vous donne d’un simple regard les techniques usuelles d’extensibilité de l’existant dans différents langages et la facilité avec laquelle celles-ci peuvent se faire selon, non seulement la nature du langage, mais aussi le paradigme de programmation sous-jacent.</p>Une histoire de slotsurn:md5:37df2868ec64fd2cb96663fca117d7a82014-03-04T11:41:00+01:002014-03-22T23:24:57+01:00Miguel MoquillonTechniques de programmationMéta-programmationprogrammation impérativeprogrammation orientée objetslot<p><img src="https://www.moquillon.fr/public/slot.png" alt="card slot" style="float:left; margin: 0 1em 1em 0;" title="card slot, mar. 2014" />L’amélioration de la structuration du code d’un programme, avec la distinction des considérations techniques de celles métier, est le Saint Graal que poursuivent sans fins les développeurs. A cette fin, de nombreuses techniques ont fait leur apparition dont nous pouvons citer les <em>traits</em> ou les <em>annotations</em>. A côté de ceux-ci, il existe une technique élégante et uniforme que sont les <em>slots</em><a href="https://www.ruby-lang.org/fr/" hreflang="fr" title="Site Web du langage Ruby"></a>. Mais, que sont ces derniers et en quoi peuvent ils nous aider dans notre quête ?</p> <p>Un adage veut qu’un bon développeur soit un développeur paresseux. Ce qui se cache derrière cette paresse est l’aptitude du codeur à automatiser ses tâches redondantes et souvent ennuyeuses et de tout faire pour faciliter son boulot. L’avantage de cette attitude est évidemment pour l’organisation un gain de productivité à terme. En fait, à y regarder de près, le développeur ne fait que s’aligner aux lois de la physique : minimiser l’entropie.
Un des aspects de cette caractéristique est, pour le programmeur, de minimiser la duplication de codes et, pour ce faire, il va user de techniques et de moyens qu’il connaît mais aussi qu’il a à sa disposition via l’outillage et surtout son langage de programmation.
La structuration du code est l’une de ces techniques. Elle est vieille de plus de 30 ans mais n’a cessé et ne cesse de continuer à évoluer. L<em>‘AOP</em> (Aspect-Oriented Programming), les <em>traits</em>, les <em>mixins</em>, les <em>closures</em>, etc. ne sont que des aspects de celle-ci dans leur objectif de modulariser non seulement le code métier mais aussi et surtout le code transversal au métier et aux fonctionnalités du logiciel. Avec l<em>‘AOP</em>, nous pouvons définir des composants d’ordre technique comme la génération de traces ou la validation des autorisations d’appel d’opérations. Ces composants sont ensuite tissés aux objets métiers à l’exécution ou à la compilation ; l<em>‘AOP</em> permet de maintenir d’un côté les caractéristiques techniques et de l’autre les modules métiers sans les entacher de considération techniques. Les <em>mixins</em> et les <em>traits</em> sont deux concepts différents permettant de définir des propriétés, d’ordre technique ou fonctionnelle, transversales aux objets métiers puis de les associer à ces derniers de manière statique ou dynamique. Par exemple, l’ordonnancement ou encore la persistance peut être définie dans un <em>mixin</em> ou un <em>trait</em>, indépendamment des objets métiers des applications. Ces deux techniques ont été popularisés respectivement par les langages <a href="https://www.ruby-lang.org/fr/" hreflang="fr" title="Le site Web du langage Ruby">Ruby</a> et <a href="http://selflanguage.org/" hreflang="en" title="Le site Web du langage Self">Self</a>. Les annotations, popularisés par <a href="http://www.java.com/fr/" hreflang="fr" title="Site Web du langage Java">Java</a>, constituent une autre technique qui permet de qualifier sémantiquement des objets et d’attacher à celles-ci un comportement qui peut aller jusqu’à la génération de codes ; par exemple, des objets peuvent être annotés avec des marqueurs de gestion des transactions ou encore d’annotations métiers propres au domaine de l’application.</p>
<p>Pourtant, il existe un autre concept popularisé par Self en même temps que les <em>traits</em> : les <em>slots</em>. Ces derniers ne sont pas nouveaux puisqu’ils furent introduit par CLOS (Common Lisp Object System). (Dans CLOS comme dans Self, ils sont appelés <em>descripteurs de slot</em>). On retrouve ces derniers dans le langage <a href="http://iolanguage.org/" hreflang="en" title="Site Web du langage IO">Io</a> et actuellement un travail est réalisé pour les intégrer à terme dans <a href="http://www.pharo-project.org/home" hreflang="en" title="Le site Web du langage Pharo">Pharo</a>. Mais qu’est-ce qu’un <em>slot</em> ou <em>descripteur de slot</em> ? Dans le contexte d’un langage orienté-objet, un <em>slot</em> est la réification du point d’accès aux propriétés d’un objet ou, plus exactement, du mécanisme d’envoi/réception de messages (cf. mon <a href="https://www.moquillon.fr/index.php/post/2012/12/21/Les-Programmations-Orient%C3%A9es-Objet">billet</a> sur les différentes POO). Tout accès aux propriétés d’un objet se fait par l’intermédiaire des slots qui, non seulement peuvent contrôler l’accès en lecture et en écriture de ces propriétés, mais aussi contenir des informations sémantiques sur celles-ci. Ils constituent une interface homogène d’accès aux propriétés de l’objet. Dans Self et <a href="http://newspeaklanguage.org/" hreflang="en" title="Le site Web du langage Newspeak">Newspeak</a>, le slot est un objet particulier qui agit comme une méthode retournant toujours une valeur (que celle-ci soit calculée dans le cas d’une méthode ou mémorisée dans le cas d’un attribut). Selon les langages de programmation, les slots peuvent n’être définis que pour l’accès aux champs d’un objet. Parce que c’est aussi un objet, le slot peut être manipulé comme n’importe quel autre objet du langage et, par voie de conséquence, il est possible alors de lui affecter aussi un comportement. Pour illustrer ceci, voici un petit exemple dans un pseudo-langage dans lequel le caractère point représente un slot (l’accès à un slot se fait par le point) :</p>
<pre>
Point.x := 3
Point.move := (abs, ord) { myself x += abs; myself y += ord; myself }
Point.move.afterInvocation := () { View current updateView(myself) }
</pre>
<p>A la première ligne, le slot réfère un champs <code>x</code> tandis qu’à la seconde ligne le slot réfère une opération <code>move</code>. Le premier retournera la valeur 3 tandis que le second, une fois son action accomplie, retournera par défaut le point lui même. La dernière ligne spécifie quoi faire à la suite de l’invocation de l’opération : mettre à jour l’IHM dans laquelle est dessiné le point. L’attribut <code>myself</code> des slots désigne l’instance de l’objet qui contient le slot. Dans la représentation ci-dessus, par homogénéité, les propriétés d’un slot (par exemple <code>afterInvocation</code>) sont elles aussi des slots (dans ce cas, il existe un slot terminal qui référence ou stocke directement la valeur de l’attribut ou les instructions de la méthode). Nous aurions pu écrire le code comme suit pour plus de lisibilité :</p>
<pre>
Point setSlot($x, 3)
Point setSlot($move, (abs, ord) { myself x += abs; myself y += ord; myself } )
Point.move setSlot($afterInvocation, () { View current updateView(myself) } )
</pre>
<p>dans lequel le signe $ désigne un symbole.</p>
<p>Parce qu’un slot peut représenter un point d’accès vers un autre objet, les relations inter-objets deviennent mécaniquement des citoyens de première classe au même titre que l’objet (ou la classe dans un langage orienté-classes). Pour illustrer ceci, imaginons un employeur qui a plusieurs employés et des employés qui est rattaché à un et un seul employeur :</p>
<pre>
Employer.employees := []
Employer.employees.onWrite := (e) { (e employer = myself) ifFalse(
{ e employer(myself) })
}
Employee.employer := None(Employer)
Employee.employer.onWrite : = (e) { (e employees contains(myself)) ifFalse(
{ e employees add(myself) })
}
</pre>
<p>Dans le code ci-dessus, chaque nouveau employé de l’employeur mettra automatiquement la référence <code>employer</code> de l’employé avec son nouveau employeur. De même, chaque mise-à-jour de l’employeur de l’employé ajoutera ce dernier parmi les employés de son nouveau employeur :</p>
<pre>
anEmployee := Employee()
anEmployer := Employer()
anEmployer employees add(anEmployee)
Assertion assertThat(anEmployee employer, is(anEmployer))
</pre>
<p>Les slots <code>employees</code> et <code>employer</code> représentent donc des relations qui peuvent être manipulés comme de simples objets et qui permettent de maintenir la cohérence des relations entre l’employeur et les employés. Nous pouvons alors introduire un mécanisme de création et de relations hiérarchiques de slots comme avec les objets (ou les classes d’objets) :</p>
<pre>
ManyToOneSlot := Slot {
oppositeSlot: Slot,
oppositeClass: Class,
initialize: (anInstance, theOppositeSlot, theOppositeClass) {
myself := anInstance
oppositeClass := System class(theOppositeClass)
oppositeSlot := oppositeClass slot(theOppositeSlot)
},
onWrite: (anInstance) {
((oppositeSlot read: anInstance) = myself ) ifFalse(
{ oppositeSlot write(myself, anInstance) } )
}
}
OneToManySlot := Slot {
oppositeSlot: Slot,
oppositeClass: Class,
initialize: (anInstance, theOppositeSlot, theOppositeClass) {
myself := anInstance
oppositeClass := System class(theOppositeClass)
oppositeSlot := oppositeClass slot(theOppositeSlot)
},
onWrite: (anInstance) {
((oppositeSlot read(anInstance)) contains(myself) ) ifFalse(
{ oppositeSlot write(myself, anInstance) } )
}
}
Employer := Object {
employees: ManyToOneSlot(self, $employer, $Employee)
}
Employee := Object {
employer: OneToManySlot(self, $employees, $Employer)
}
</pre>
<p>De la même façon, il est alors possible de définir des slots dédiés à la persistance ou encore de combiner les slots entre eux pour réaliser des traitements transverses. Ceci et le fait d’ajouter du comportement aux slots ouvrent la porte à la méta-programmation, à la réflexion, sans commune mesure avec les autres concepts. Ce que permettent l’AOP, les annotations, certains usages des mixins et des traits, peuvent toutes être réalisés par les slots et ceci d’une manière uniforme et cohérente avec les concepts de base du langage.</p>
<p>Ce petit billet vous a donné un petite présentation du concept de <em>slot</em>, illustrée avec un pseudo-langage. J’espère qu’il vous aura donné un aperçu de la puissance et de l’expressivité qui se cache derrière ce simple concept.</p>Langages impératifs et fonctionnelsurn:md5:8e70fabeeb536a0ab0e21fb2711a1da02012-10-31T18:21:00+00:002012-11-22T22:12:23+00:00Miguel MoquillonLangagesJavaprogrammation fonctionnelleprogrammation impérativeScala<p>James Roper a publié sur son blog un billet qui compare <a href="https://jazzy.id.au/default/2012/10/16/benchmarking_scala_against_java.html" hreflang="en" title="Performances de Scala vis à vis de Java">les performances de Java et de Scala</a> via une réalisation du tri rapide d’une liste ou d’un tableau (<em>quicksort</em>). J’ai trouvé l’article intéressant pour deux raisons principales et qui sont liées à la nature particulière de ces deux langages.</p> <p>Le premier aspect que j’ai trouvé intéressant dans <a href="https://jazzy.id.au/default/2012/10/16/benchmarking_scala_against_java.html" hreflang="en" title="Les performances de Scala vis à vis de Java">le billet de Jon Roper</a> 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 !</p>
<p>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).</p>
<p>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 <em>tail recursivity</em> (en gros, remplacement de la pile courante par celle de la fonction appelée).</p>
<p>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 :</p>
<pre>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 !).</pre>
<p>Ce à quoi, les tenants des langages impératifs sourient, haussent les épaulent, et rétorquent qu’au contraire l’humain pense séquentiellement :</p>
<pre>
* à 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.</pre>
<p>Un autre exemple qui illustre bien cet aspect séquentiel est évidemment la recette de cuisine.</p>
<p>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.</p>
<p>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.</p>