points de vue - Mot-clé - Haskellles 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>Foncteurs, Foncteurs Applicatifs et Monadesurn:md5:8fce5bcf1531c6333026cf4403425a632018-01-21T19:58:00+01:002018-04-21T18:56:56+02:00Miguel MoquillonTechniques de programmationfoncteurHaskellJavamonadeprogrammation fonctionnelle<p>Dans ce premier billet de l’année 2018, je vais m’essayer de vous présenter ce que sont les foncteurs, les foncteurs applicatifs et les monades de façon simple, sans étalage de la théorie mathématique derrière (celle des catégories) dont, de toute manière, je ne maîtrise pas. Bien que ce soient des constructions utilisées dans la programmation fonctionnelle, elles peuvent aussi être utilisées dans d’autres approches de programmation et avec d’autres langages que ceux fonctionnels. C’est pourquoi je présenterai chacun des concepts non seulement avec du code en Haskell mais aussi en Java.</p> <p>Tout paradigme de programmation définit des règles de composition, ceci afin de mieux structurer le code, faciliter son écriture et sa maintenance, et pour éviter de lourdes duplications de code. Ces règles permettent de représenter des concepts, des abstractions de plus haut niveau, par la composition de briques logiciels plus techiques ou d’abstractions plus faibles. Dans les langages dits impératifs, les règles de composition sont centrées sur les structures de données ; par exemple, l’extension de classes ou de modules. Dans le paradigme fonctionnel, l’accent est mit sur la composition de fonctions ; l’exemple le plus connu est le fameux <em>“f rond g</em>” qui permet d’obtenir une nouvelle fonction par connection de deux autres (l’entrée d’une fonction est <i>connectée</i> à la sortie d’une autre). C’est probablement la raison principale pour laquelle le nom de <em>fonctionnel</em> est donné à cette approche de programmation.</p>
<p>Les foncteurs, les foncteurs applicatifs et les monades ne sont en faits que d’autres constructions de composition, d’abstraction plus élevées. Elles sont inspirées de la théorie des catégories en Mathématiques, d’où leur nom, mais ne nécessitent pas en réalité une connaissance de celle-ci pour pouvoir les utiliser. Elles sont en général définies dans les langages fonctionnels à l’aide des types algébriques. Leur objectif est, grosso-modo, de pouvoir chaîner des fonctions autour d’objets transportant un contexte informationnel ou calculatoire et de conserver celui-ci au travers de cet enchaînement. L’illustration la plus connue est le resultat des opérations à effet de bord comme, par exemple, l’affichage d’un texte à l’écran ou l’accès à une donnée dans une mémoire transactionnelle.</p>
<p>Dans ce qui suit, afin d’introduire ces différentes constructions, je vous propose un exemple simple de calcul à partir d’un nombre que donne l’utilisateur. Je ne détaillerai pas le code en Java, celui-ci étant d’inspiration impérative, approche que ~95% des programmeurs utilisent. Voici ce que donnerait un tel code en Haskell :</p>
<pre class="brush: haskell">
main = do
putStrLn "Give me an integer"
intAsStr <- getLine
let int = read intAsStr :: Int
putStrLn $ show $ int * int
</pre>
<p>Pour les lecteurs ne connaissant pas Haskell, le symbole <code>$</code> est un raccourcis de la mise en parenthèses de ce qui suit ; il indique que le sens de l’évaluation doit se faire de droite à gauche. La fonction <code>read</code> ici convertit l’entrée de l’utilisateur en entier (indiqué par l’instruction <code>:: Int</code>) tandis que la fonction <code>show</code> fait l’opération inverse (il convertit son argument en une chaîne de caractères). (Nous pouvons toutefois remplacer l’instruction <code>putStrLn $ show</code> par la fonction <code>print</code>)</p>
<p>En Java on aurait ceci :</p>
<pre class="brush: java">
public static void main(String[] args) {
System.out.println("Give me an integer");
Scanner scanner = new Scanner(System.in);
final int number = scanner.nextInt();
System.out.println(number * number);
}
</pre>
<p>Dans cet exemple, nous multiplions l’entier donné par l’utilisateur par lui-même. Tant que l’utilisateur passe un entier tout se passe bien. Mais si jamais celui-ci se trompe et donne, par exemple, des lettres, on aurait aussitôt une exception qui interromprait le programme :</p>
<pre class="brush: bash">
Give me an integer
e4
simple: Prelude.read: no parse
</pre>
<p>et avec notre programme en Java :</p>
<pre class="brush: bash">
Give me an integer
e4
Exception in thread "main" java.util.InputMismatchException
at java.util.Scanner.throwFor(Scanner.java:864)
at java.util.Scanner.next(Scanner.java:1485)
at java.util.Scanner.nextInt(Scanner.java:2117)
at java.util.Scanner.nextInt(Scanner.java:2076)
at fr.moquillon.fonctors.Simple.main(Simple.java:10)
</pre>
<p>Pour éviter ce comportement indésirable, en général, il suffit de vérifier ce que donne l’utilisateur et d’agir en conséquence. Ce qui donnerai en Haskell :</p>
<pre class="brush: haskell">
displayResultIfInt i
| isInt i = print $ (read i ::Int) * (read i :: Int)
| otherwise = putStrLn "You don't give me an integer :-("
where
isInt s = case reads s :: [(Int, String)] of
[(_, "")] -> True
_ -> False
main = do
putStrLn "Give me an integer"
str <- getLine
displayResultIfInt str
</pre>
<p>et en Java :</p>
<pre class="brush: java">
private static void displayResultIfInt(final Scanner reader) {
if (reader.hasNextInt()) {
int i = reader.nextInt();
System.out.println(i * i);
} else {
System.out.println("You don't give me an integer :-(");
}
}
public static void main(String args[]) {
System.out.println("Give me an integer");
Scanner scanner = new Scanner(System.in);
displayResultIfInt(scanner);
}
</pre>
<p>Pou rester simple, le programme ne fait que sortir un message d’erreur si l’utilisateur donne autre chose qu’un entier :</p>
<pre class="brush: bash">
Give me an integer
e4
You don't give me an integer :-(
</pre>
<p>Toutefois, il y a une autre façon de contrôler les effets de bords indésirables : encapsuler ce que passe l’utilisateur dans une boite afin d’y contraindre les effets de bords et donc éviter qu’elles se répandent dans le reste du programme. Pour notre exemple, si l’utilisateur donne bien un entier, la boite aura cette valeur, sinon elle sera vide puisque, dans notre cas, on ne fera rien avec. Soit en Haskell :</p>
<pre class="brush: haskell">
readInt i
| isInt i = Just (read i :: Int)
| otherwise = Nothing
where
isInt s = case reads s :: [(Int, String)] of
[(_, "")] -> True
_ -> False
main = do
putStrLn "Give me an integer"
intAsStr <- getLine
let maybeInt = readInt intAsStr
case maybeInt of
Nothing -> putStrLn "You don't give me an integer :-("
Just i -> print $ i * i
</pre>
<p>Ici, on encapsule ce que passe l’utilisateur dans un objet <code>Maybe</code> qui peut avoir alors deux contextes distinctes : soit il a rien (<code>Nothing</code>), soit il a juste la valeur donnée par l’utilisateur. Voici l’équivalent en Java :</p>
<pre class="brush: java">
private static Optional<integer> readInt(final Scanner reader) {
if (reader.hasNextInt()) {
return Optional.of(reader.nextInt());
} else {
return Optional.empty();
}
}
public static void main(String args[]) {
System.out.println("Give me an integer");
final Optional<integer> optionalInt = readInt(new Scanner(System.in));
if (optionalInt.isPresent()) {
System.out.println(optionalInt.get() * optionalInt.get());
} else {
System.out.println("You don't give me an integer :-(");
}
}
</pre>
<p>Nous remarquons ici que nous avons mieux structuré notre programme. On contraint tout effet de bord dans une boite (<code>Maybe</code> en Haskell, <code>Optional</code> en Java) et on aiguille le contrôle du programme selon l’état de cette boite. Avant de poursuivre, il existe en Haskell une fonction qui fait déjà ce que notre <code>readInt</code> accomplit mais de façon plus générique : <code>readMaybe</code></p>
<pre class="brush: haskell">
import Text.Read (readMaybe)
main = do
putStrLn "Give me an integer"
intAsStr <- getLine
let maybeInt = readMaybe intAsStr :: Maybe Int
case maybeInt of
Nothing -> putStrLn "You don't give me an integer :-("
Just i -> print $ i * i
</pre>
<p>Maintenant, structurons un peu plus notre programme en y représentant d’une part notre calcul par une fonction spécifique que l’on va appeler <code>compute</code> et d’autre part le traitement du résultat par une autre fonction (ici <code>display</code>) :</p>
<pre class="brush: haskell">
import Text.Read (readMaybe)
compute i = i * i
display result =
case result of
Nothing -> putStrLn "You don't give me an integer :-("
Just number -> print number
main = do
putStrLn "Give me an integer"
intAsStr <- getLine
let result =
case (readMaybe intAsStr :: Maybe Int) of
Nothing -> Nothing
Just i -> Just $ compute i
display result
</pre>
<p>et en Java :</p>
<pre class="brush: java">
private static Optional<integer> readInt(final Scanner reader) {
if (reader.hasNextInt()) {
return Optional.of(reader.nextInt());
} else {
return Optional.empty();
}
}
private static long compute(int i) {
return i * i;
}
private static void display(final Optional<Long> result) {
if (result.isPresent()) {
System.out.println(result.get());
} else {
System.out.println("You don't give me an integer :-(");
}
}
public static void main(String args[]) {
System.out.println("Give me an integer");
final Optional<Integer> optionalInt = readInt(new Scanner(System.in));
Optional<Long> result;
if (optionalInt.isPresent()) {
result = Optional.of(compute(optionalInt.get()));
} else {
result = Optional.empty();
}
display(result);
}
</pre>
<p>Nous remarquons que le traitement conditionnel de l’état de notre boite (<code>Maybe</code> en Haskell et <code>Optional</code> en Java) est dupliqué. Ce serait bien d’éviter ceci. En regardant bien le code, nous constatons que le calcul est appliqué que si la boite n’est pas vide, sinon la boite vide est retournée à nouveau. Bref cette dernière instruction apparaît bien inutile. Ce qu’il faudrait ici est une fonction qui préserve la structure de notre boite. La fonction <code>compute</code> étant dans notre cas une opération métier, elle n’a pas à être modifiée pour prendre en compte les particularités de notre code. Il faudrait alors une fonction qui puisse appliquer notre fonction de calcul à la boite même tout en préservant sa structure, sa propre structure de boite : elle retournerait soit une boite vide si notre boite est vide, soit une boite avec le résultat du calcul. Or, ça tombe bien, il existe une telle fonction aussi bien en Haskell qu’en Java (à partir de Java 8) : c’est la fonction <code>fmap</code> en Haskell (qui définit aussi son équivalent en opérateur : <code><$></code>) :</p>
<pre class="brush: haskell">
import Text.Read (readMaybe)
compute i = i * i
display result =
case result of
Nothing -> putStrLn "You don't give me an integer :-("
Just number -> print number
main = do
putStrLn "Give me an integer"
intAsStr <- getLine
let result = fmap compute (readMaybe intAsStr :: Maybe Int)
-- ou let result = compute <$> (readMaybe intAsStr :: Maybe Int)
display result
</pre>
<p>et c’est la méthode <code>map</code> en Java :</p>
<pre class="brush: java">
public class FunctorExample {
private static Optional<integer> readInt(final Scanner reader) {
if (reader.hasNextInt()) {
return Optional.of(reader.nextInt());
} else {
return Optional.empty();
}
}
private static long compute(int i) {
return i * i;
}
private static void display(final Optional<Long> result) {
if (result.isPresent()) {
System.out.println(result.get());
} else {
System.out.println("You don't give me an integer :-(");
}
}
public static void main(String args[]) {
System.out.println("Give me an integer");
final Optional<Integer> optionalInt = readInt(new Scanner(System.in));
Optional<Long> result = optionalInt.map(FunctorExample::compute);
display(result);
}
}
</pre>
<p>Voilà, vous venez d’écrire votre premier <em>foncteur</em> ! En effet, un foncteur n’est rien d’autre qu’une fonction qui applique une autre fonction à des objets dotés d’une structure et qui préserve cette structure. Par analogie avec la théorie des catégories, les objets ici sont des morphismes, en l’occurrence <code>Maybe Int</code> en Haskell pour lesquels <code>fmap</code> conserve bien leur structure de <code>Maybe</code>. Un foncteur est donc toujours relatif à un type de données générique. En Haskell, les foncteurs sont explicites. Ils sont définis par le type algébrique <code>Functor</code> et <code>Maybe</code> est une instance de ce type et spécifie donc comment <code>fmap</code> s’applique à elle :</p>
<pre class="brush: haskell">
class Functor f where
fmap :: (a -> b) -> f a -> f b
-- | Replace all locations in the input with the same value.
-- The default definition is @'fmap' . 'const'@, but this may be
-- overridden with a more efficient version.
(<$) :: a -> f b -> f a
(<$) = fmap . const
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)
</pre>
<p>On dit alors, par extension, que <code>Maybe</code> est un foncteur. Aussi, nous conserverons par la suite cette dénomination aussi bien à la fonction <code>fmap</code> (<code>map</code> en Java) qu’à l’objet sur lequel elle s’applique. En Haskell, les fonctions aussi sont des foncteurs (illustrés ici via le shell interactif Ghci) :</p>
<pre class="brush: bash">
$ stack ghci
Configuring GHCi with the following packages:
GHCi, version 8.0.2: http://www.haskell.org/ghc/ :? for help
Loaded GHCi configuration from /tmp/ghci5585/ghci-script
Prelude> let i = 3
Prelude> let fun = fmap (*i) (*i)
Prelude> fun 10
90
</pre>
<p>En Java, les foncteurs sont quant à eux définis implicitement via la méthode <code>map</code>. Nous pouvons donc dire, par abus de langage, que <code>Optional</code> est implicitement un foncteur (un type applicable par un foncteur). Par contre, contrairement en Haskell, les listes en Java ne sont pas sujettes aux foncteurs mais plutôt les <code>Stream</code>, qui sont une construction générique d’encapsuler des données et d’y appliquer pseudo-paresseusement une chaîne de traitement.</p>
<p>Passons maintenant à la vitesse supérieure et modifions notre opération <code>compute</code> de façon à ce qu’elle accepte désormais deux paramètres au lieu d’un seul. Nous nous trouvons face à une situation où nous ne pouvons plus utiliser notre foncteur tel quel car il ne peut s’appliquer que sur une fonction à un seul paramètre, ce paramètre censé recevoir l’objet encapsulé par notre boite. Voyons ce que donne l’application de <code>fmap</code> avec une fonction à deux paramètres sur notre objet <code>Maybe Int</code> :</p>
<pre class="brush: bash">
$ stack ghci
Configuring GHCi with the following packages:
GHCi, version 8.0.2: http://www.haskell.org/ghc/ :? for help
Loaded GHCi configuration from /tmp/ghci5585/ghci-script
Prelude> i = 3
Prelude> compute i j = i * j
Prelude> :type fmap compute
fmap compute :: (Num a, Functor f) => f a -> f (a -> a)
Prelude> :type fmap compute (Just i)
fmap compute (Just i) :: Num a => Maybe (a -> a)
</pre>
<p>Nous remarquons qu’une autre boite est retournée avec … une fonction à un paramètre dedans. En Haskell, toute fonction à plusieurs paramètres peut se résumer à une fonction à un seul paramètre qui retourne une fonction sur les autres paramètres ; c’est ce que l’on appelle la curryfication (du nom du mathématicien Haskell Curry) et <code>fmap</code> ne fait rien d’autre que curryfier la fonction <code>compute</code> et comme elle préserve la structure de <code>Maybe</code>, elle retourne donc ici un <code>Maybe (Int -> Int)</code> (une boite <code>Maybe</code> avec une fonction à un paramètre dedans). En partant de ce constat, on pourrait étendre notre code existant avec une fonction qui sache appliquer une fonction encapsulée dans une boite (en l’occurrence un <code>Maybe (Int -> Int)</code>) sur notre <code>Maybe Int</code>. Or, justement il existe une telle fonction en Haskell (ou plus exactement un tel opérateur) : <code><*></code>.</p>
<pre class="brush: haskell">
import Text.Read (readMaybe)
compute :: Int -> Int -> Int
compute i j = i * j
display result =
case result of
Nothing -> putStrLn "You don't give me an integer :-("
Just number -> print number
main = do
putStrLn "Give me an integer"
intAsStr <- getLine
let maybeInt = readMaybe intAsStr :: Maybe Int
let result = fmap compute maybeInt <*> maybeInt
display result
</pre>
<p>Vous venez là aussi d’écrire votre premier foncteur applicatif ! En effet, <code><*></code> n’est rien d’autre qu’un <em>foncteur applicatif</em> (<em>applicative</em> en anglais). Sachant qu’ici <code>fmap compute</code> définit en quelque part un type de foncteurs, on peut écrire qu’un foncteur applicatif est un foncteur de foncteurs. Maintenant, comment écrire ceci en Java, sachant que les foncteurs applicatifs et la curryfication n’y existent pas. Nous pouvons nous inspirer du code Haskell pour accomplir la même chose :</p>
<pre class="brush: java">
public class ApplicativeExample {
private static Optional<Integer> readInt(final Scanner reader) {
if (reader.hasNextInt()) {
return Optional.of(reader.nextInt());
} else {
return Optional.empty();
}
}
private static long compute(int i, int j) {
return i * j;
}
private static Function<Integer, Long> curryfiedCompute(final int i) {
return j -> compute(i, j);
}
private static Optional<Function<Integer, Long>> computeFunctor(Optional<Integer> optionalInt) {
return optionalInt.map(ApplicativeExample::curryfiedCompute);
}
private static <T, R> Optional<R> applicativeMap(Optional<Function<T, R>> optionalFunction,
Optional<T> optionalParameter) {
if (optionalParameter.isPresent() && optionalFunction.isPresent()) {
return Optional.ofNullable(optionalFunction.get().apply(optionalParameter.get()));
} else {
return Optional.empty();
}
}
private static void display(final Optional<Long> result) {
if (result.isPresent()) {
System.out.println(result.get());
} else {
System.out.println("You don't give me an integer :-(");
}
}
public static void main(String args[]) {
System.out.println("Give me an integer");
final Optional<Integer> optionalInt = readInt(new Scanner(System.in));
Optional<Long> result = applicativeMap(computeFunctor(optionalInt), optionalInt);
display(result);
}
}
</pre>
<p>Pour simplifier le code et rendre réutilisable les foncteurs applicatifs, nous pouvons réécrire la classe <code>Optional</code> pour qu’elle puisse supporter les foncteurs applicatifs :</p>
<pre class="brush: java">
public class ApplicativeOptional<T> {
...
public <U> ApplicativeOptional<U> appMap(ApplicativeOptional<Function<? super T, ? extends U>> mapper) {
Objects.requireNonNull(mapper);
if (isPresent() && mapper.isPresent()) {
return ofNullable(mapper.get().apply(value));
} else {
return empty();
}
}
...
}
</pre>
<p>et utiliser cette nouvelle classe dans notre exemple :</p>
<pre class="brush: java">
public class ApplicativeExample {
private static ApplicativeOptional<Integer> readInt(final Scanner reader) {
if (reader.hasNextInt()) {
return ApplicativeOptional.of(reader.nextInt());
} else {
return ApplicativeOptional.empty();
}
}
private static long compute(int i, int j) {
return i * j;
}
private static Function<Integer, Long> curryfiedCompute(final int i) {
return j -> compute(i, j);
}
private static void display(final ApplicativeOptional<Long> result) {
if (result.isPresent()) {
System.out.println(result.get());
} else {
System.out.println("You don't give me an integer :-(");
}
}
public static void main(String args[]) {
System.out.println("Give me an integer");
final ApplicativeOptional<Integer> optionalInt = readInt(new Scanner(System.in));
ApplicativeOptional<Long> result = optionalInt.appMap(optionalInt.map(ApplicativeExample::curryfiedCompute));
display(result);
}
}
</pre>
<p>En Haskell, comme pour les foncteurs, les foncteurs applicatifs sont définis par un type algébrique, ici <code>Applicative</code> et là aussi <code>Maybe</code> est une instance de celui-ci :</p>
<pre class="brush: haskell">
class Functor f => Applicative f where
{-# MINIMAL pure, ((<*>) | liftA2) #-}
-- | Lift a value.
pure :: a -> f a
-- | Sequential application.
--
-- A few functors support an implementation of '<*>' that is more
-- efficient than the default one.
(<*>) :: f (a -> b) -> f a -> f b
(<*>) = liftA2 id
-- | Lift a binary function to actions.
--
-- Some functors support an implementation of 'liftA2' that is more
-- efficient than the default one. In particular, if 'fmap' is an
-- expensive operation, it is likely better to use 'liftA2' than to
-- 'fmap' over the structure and then use '<*>'.
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
liftA2 f x = (<*>) (fmap f x)
...
instance Applicative Maybe where
pure = Just
Just f <*> m = fmap f m
Nothing <*> _m = Nothing
liftA2 f (Just x) (Just y) = Just (f x y)
liftA2 _ _ _ = Nothing
Just _m1 *> m2 = m2
Nothing *> _m2 = Nothing
</pre>
<p>Nous constatons que pour qu’un type soit applicable par un foncteur applicatif, il faut qu’il le soit déjà par un foncteur (logique !). Par extension, on dit que <code>Maybe</code> est aussi un foncteur applicatif. On remarque aussi l’existence d’une fonction <code>liftA2</code>, basée sur <code><*></code>, qui permet de simplifier l’écriture de notre programme en prenant en compte directement les fonctions à deux paramètres et les appliquer à deux foncteurs :</p>
<pre class="brush: haskell">
import Control.Applicative (liftA2)
import Text.Read (readMaybe)
compute i j = i * j
display result =
case result of
Nothing -> putStrLn "You don't give me an integer :-("
Just number -> print number
main = do
putStrLn "Give me an integer"
intAsStr <- getLine
let maybeInt = readMaybe intAsStr :: Maybe Int
let result = liftA2 compute maybeInt maybeInt
display result
</pre>
<p>De la même manière, nous pourrions aussi écrire en Java un équivalent à <code>liftA2</code> :</p>
<pre class="brush: java">
public class ApplicativeOptional<T> {
...
public <U, R> ApplicativeOptional<R> biLift(final BiFunction<T, U, R> mapper, ApplicativeOptional<U> param) {
if (param.isPresent() && this.isPresent()) {
return ApplicativeOptional.ofNullable(mapper.apply(this.get(), param.get()));
} else {
return ApplicativeOptional.empty();
}
}
...
</pre>
<p>et l’utiliser dans notre programme :</p>
<pre class="brush: java">
public static void main(String args[]) {
System.out.println("Give me an integer");
final ApplicativeOptional<Integer> optionalInt = readInt(new Scanner(System.in));
ApplicativeOptional<Long> result = optionalInt.biLift(ApplicativeExample::compute, optionalInt);
display(result);
}
</pre>
<p>Nous avons donc vu ce qu’était un foncteur et un foncteur applicatif (un foncteur de foncteurs) et à quels usages ils répondaient. Un foncteur pourra être utilisé pour appliquer des fonctions à des objets dotés d’une structure interne et conserver celle-ci sans avoir à accéder directement aux détails de cette structure. De tels objets sont, comme nous l’avons vu, en général des types génériques qui véhiculent un contexte. Un foncteur applicatif n’est rien d’autre qu’une extension des foncteurs aux fonctions mêmes et permet de pouvoir appliquer une boite avec une fonction sur une boite avec une valeur. Ceci est utile lorsqu’il s’agit d’appliquer, comme dans notre exemple, des fonctions à plusieurs paramètres sur un objet dotés d’une structure, mais il peut être aussi utilisé avec des fonctions obtenues à partir d’un autre traitement contextuel.</p>
<p>Ne nous arrêtons pas pour autant là et continuons de faire évoluer notre programme. Imaginons maintenant que notre fonction <code>compute</code> a un comportement distinct en fonction de ses paramètres. Par exemple, qu’elle n’effectue son opération qu’avec les entiers positifs :</p>
<pre class="brush: haskell">
compute :: Int -> Int -> Maybe Int
compute i j
| i < 0 || j < 0 = Nothing
| otherwise = Just $ i * j
</pre>
<p>Cette fois ci, <code>compute</code> retourne une boite avec potentiellement le résultat du calcul, sinon rien. A première vue <code>fmap compute </code> devrait retourner un foncteur avec notre fonction <code>compute</code> curryfiée. Or comme cette dernière accepte comme paramètre un simple entier, l’opérateur <code><*></code> devrait pouvoir l’appliquer à notre <code>Maybe Int</code>. Sachant de plus que l’opérateur <code><*></code> préserve la structure de notre boite <code>Maybe Int</code> auquel il est appliqué, et que la fonction curryfiée retourne elle-même une boite, on est en droit à s’attendre à ce que l’application de l’opérateur retourne un <code>Maybe (Maybe Int)</code>, résultat que ne saurait interpréter la fonction <code>display</code> telle qu’elle est écrite. Vérifions ceci :</p>
<pre class="brush: haskell">
Configuring GHCi with the following packages:
GHCi, version 8.0.2: http://www.haskell.org/ghc/ :? for help
Loaded GHCi configuration from /tmp/ghci9751/ghci-script
Prelude> compute i j | i < 0 && j < 0 = Nothing | otherwise = Just $ i * j
Prelude> fmap compute (Just 3) <*> Just 3
Just (Just 9)
</pre>
<p>C’est bien le cas. Or, ce que l’on voudrait c’est en retour un <code>Maybe Int</code>. L’idée ici est soit de trouver une fonction en lieu et place de <code><*></code> qui fasse ça tout seul, soit étendre comme précédemment notre composition fonctionnelle en y introduisant une autre fonction qui, à partir d’une boite de boite, retourne la boite contenue. Et ça tombe bien parce qu’Haskell fournit une telle fonction, ou plus exactement un tel opérateur (là aussi) : <code>>>=</code>. Voyons ce que cela donne :</p>
<pre class="brush: haskell">
import Text.Read (readMaybe)
compute :: Int -> Int -> Maybe Int
compute i j
| i < 0 || j < 0 = Nothing
| otherwise = Just $ i * j
display result =
case result of
Nothing -> putStrLn "You don't give me a positive integer :-("
Just number -> print number
main = do
putStrLn "Give me a positive integer"
intAsStr <- getLine
let maybeInt = readMaybe intAsStr :: Maybe Int
let result = fmap compute maybeInt <*> maybeInt >>= id
display result
</pre>
<p>Afin d’obtenir juste ce que contient le <code>Maybe (Maybe Int)</code>, c’est-à-dire le <code>Maybe Int</code>, nous appliquons l’opérateur <code>>>=</code> directement sur la fonction identité <code>id</code>. Il existe toutefois une fonction qui fait déjà ceci : <code>join</code>.</p>
<pre class="brush: haskell">
import Text.Read (readMaybe)
import Control.Monad (join)
compute :: Int -> Int -> Maybe Int
compute i j
| i < 0 || j < 0 = Nothing
| otherwise = Just $ i * j
display result =
case result of
Nothing -> putStrLn "You don't give me a positive integer :-("
Just number -> print number
main = do
putStrLn "Give me a positive integer"
intAsStr <- getLine
let maybeInt = readMaybe intAsStr :: Maybe Int
let result = join $ fmap compute maybeInt <*> maybeInt
display result
</pre>
<p>Voilà, nous venons d’introduire le concept de <em>monade</em>. Une monade n’est pas cette fois-ci un morphisme comme le sont les foncteurs et les foncteurs applicatifs mais plutôt une construction, un type algébrique abstrait, reposant sur les foncteurs et qui est défini par les propriétés suivantes :</p>
<ul>
<li>il existe une correspondance qui à tout type générique relie un type monadique ; autrement dit, par simplification, un constructeur qui à une valeur retourne une monade avec cette valeur. Dans notre exemple, <code>fmap compute</code> retourne une monade avec un tel constructeur (la fonction <code>compute</code> curryfiée)</li>
<li>il existe une opération de composition interne associative entre monades sous forme d’un foncteur, donc qui préserve la structure monadique. Dans notre exemple, il s’agit de <code>>>=</code></li>
<li>il existe un élément neutre, appelé identité. Dans le cas des <code>Maybe a</code>, c’est <code>Nothing</code>.</li>
</ul>
<p>Une <em>monade</em> est une application aux catégories (en gros, dans notre cas, aux foncteurs) ce que sont les monoïdes en algèbre (qui sont des ensembles munis d’une loi de composition interne associative et d’un élément neutre). D’où le nom de monade. En Haskell, comme pour les foncteurs et les foncteurs applicatifs, les monades sont définis explicitement par un type algébrique, <code>Monad</code> et, comme on s’en est douté, <code>Maybe</code> est aussi une instance de celui-ci :</p>
<pre class="brush: haskell">
class Applicative m => Monad m where
-- | Sequentially compose two actions, passing any value produced
-- by the first as an argument to the second.
(>>=) :: forall a b. m a -> (a -> m b) -> m b
-- | Sequentially compose two actions, discarding any value produced
-- by the first, like sequencing operators (such as the semicolon)
-- in imperative languages.
(>>) :: forall a b. m a -> m b -> m b
m >> k = m >>= \_ -> k -- See Note [Recursive bindings for Applicative/Monad]
{-# INLINE (>>) #-}
-- | Inject a value into the monadic type.
return :: a -> m a
return = pure
...
instance Monad Maybe where
(Just x) >>= k = k x
Nothing >>= _ = Nothing
(>>) = (*>)
...
</pre>
<p>Donc, pour qu’un type soit une monade il faut qu’il soit applicable par les foncteurs applicatifs et donc par les foncteurs. En l’occurrence, <code>Maybe</code> est une monade.</p>
<p>A l’image des foncteurs applicatifs, il existe une méthode <code>liftM2</code> que nous pouvons ici utiliser pour simplifier notre code :</p>
<pre class="brush: haskell">
import Text.Read (readMaybe)
import Control.Monad (liftM2)
compute :: Int -> Int -> Maybe Int
compute i j
| i < 0 || j < 0 = Nothing
| otherwise = Just $ i * j
display result =
case result of
Nothing -> putStrLn "You don't give me a positive integer :-("
Just number -> print number
main = do
putStrLn "Give me a positive integer"
intAsStr <- getLine
let maybeInt = readMaybe intAsStr :: Maybe Int
let result = liftM2 compute maybeInt maybeInt
display result
</pre>
<p>N’avez vous pas trouvé une ressemblance de l’opérateur <code>>>=</code> avec celui <code><$></code>, autrement dit avec la fonction équivalente <code>fmap</code> ? En fait, tout simplement, l’opérateur <code>>>=</code> n’est rien d’autre que ce que l’on appelle un <em>flat map</em>. En partant de ce constat, il est alors relativement simple d’écrire un code équivalent en Java. La classe <code>Optional</code> dispose de la méthode <code>flatMap</code> mais ne supporte pas les foncteurs applicatifs. Aussi créons une nouvelle classe, <code>MonadicOptional</code>, qui soit sujet non seulement aux foncteurs applicatifs mais aussi au <em>flat map</em> :</p>
<pre class="brush: java">
public final class MonadicOptional<T> {
...
public <U> MonadicOptional<U> appMap(MonadicOptional<Function<? super T, ? extends U>> mapper) {
Objects.requireNonNull(mapper);
if (isPresent() && mapper.isPresent()) {
return ofNullable(mapper.get().apply(value));
} else {
return empty();
}
}
public <u> MonadicOptional<U> flatMap(Function<? super T, MonadicOptional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(get()));
}
}
...
}
</pre>
<p>puis utilisons le dans notre programme :</p>
<pre class="brush: java">
public class MonadExample {
private static MonadicOptional<Long> compute(int i, int j) {
if (i < 0 && j < 0) {
return MonadicOptional.empty();
}
return MonadicOptional.ofNullable((long) (i * j));
}
private static Function<Integer, MonadicOptional<Long>> curriedCompute(final int i) {
return j -> compute(i, j);
}
private static MonadicOptional<Integer> readInt(final Scanner reader) {
if (reader.hasNextInt()) {
return MonadicOptional.of(reader.nextInt());
} else {
return MonadicOptional.empty();
}
}
private static void display(final MonadicOptional<Long> result) {
if (result.isPresent()) {
System.out.println(result.get());
} else {
System.out.println("You don't give me an integer :-(");
}
}
public static void main(String args[]) {
System.out.println("Give me an integer");
final MonadicOptional<Integer> optionalInt = readInt(new Scanner(System.in));
MonadicOptional<long> result = optionalInt.appMap(optionalInt.map(MonadExample::curriedCompute)).flatMap(Function.identity());
display(result);
}
}
</pre>
<p>En conclusion, les foncteurs et les foncteurs applicatifs sont les briques de base nécessaire à la conception des monades dont l’intérêt est finalement de pouvoir chaîner des actions sur un type générique doté d’une structure tout en conservant celle-ci. Les monades établissent l’ensemble des propriétés que doivent vérifier de tels types génériques, permettant ainsi de les manipuler sans avoir à casser, pour ce faire, leur structure et répandre ainsi leur contexte dans tout le programme (les effets de bords), perdant ainsi potentiellement le contrôle sur celui-ci et ouvrant la voie à des bogues potentiels, et limitant aussi l’évolution et l’extension du code.</p>
<p>Vous comprenez maintenant aussi d’où proviennent les <em>map</em> et les <em>flat map</em> et, par extension, le <em>map-reduce</em>. Il est toutefois dommage que de telles constructions, en provenance de la programmation fonctionnelle, soit récupérées et intégrées dans les langages impératifs, comme Java, sans comprendre, et donc prendre en compte, l’essence même de celles-ci. C’est ce qui explique pourquoi elles apparaîssent si limitées et donc fustrantes à utiliser dans certains contextes avec des langages comme Java.</p>
<p>Pour finir, j’espère que ce billet aura été explicite dans sa présentation de ce que sont les foncteurs, les foncteurs applicatifs, et les monades et que de là vous ayez une meilleur compréhension de ce qu’ils peuvent apporter dans vos programmes.</p>Evolution de code avec Haskell (partie 2) : évolutionurn:md5:f9849d2454e95c7ea51e269ce6491ddc2013-05-25T10:20:00+02:002016-09-08T07:52:16+02:00Miguel MoquillonLangagesHaskellprogrammation fonctionnelle<p>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é.</p>
<p>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.</p> <p>Le programme sur lequel nous allons porter notre attention est un calculateur du résultat d’une bataille pour le jeu King’s Empire. Ce dernier est un jeu MMORPG (Massively Multiplayer Online Role Playing Game) pour smartphone et tablette disponible aussi bien sur iOS que sur Androïd.</p>
<p>Cette partie portera sur les capacités d’évolution du code avec un langage fonctionnel (ici Haskell).</p>
<p>Pour commencer, introduisons une évolution dans le programme sous la forme d’une nouvelle protection des villes : les tours d’archers. Celles-ci sont définie par leur nombre et leur niveau :</p>
<pre class="brush: erl">data Towers = Towers Int Int
deriving (Show, Read)</pre>
<p>Les tours ne peuvent attaquer mais présentent les mêmes caractéristiques défensives que les troupes. Une première approche serait alors que les tours satisfassent la classe de types <code>Fighter</code> avec 0 comme valeur d’attaque. Or, la tour d’archer est une unité avant tout défensive de la ville et non de combat. Il serait alors intéressant de découper la classe en deux, chacune représentant une caractéristique d’une unité militaire : l’attaque et la défense : <code>Attacker</code> et <code>Defender</code>.</p>
<pre class="brush: erl">class Attacker a where
kind :: a -> TroopClass
attack :: a -> Int
class Defender a where
infDef :: a -> Int
cavDef :: a -> Int
magDef :: a -> Int
artDef :: a -> Int</pre>
<p>Nos troupes sont à la fois des attaquants et des défenseurs tandis que les tours d’archers ne sont que des défenseurs :</p>
<pre class="brush: erl">instance Attacker Troop where
....
instance Defender Troop where
...
instance Defender Towers where
infDef (Towers 1 count) = 500 * count
infDef (Towers 2 count) = 2000 * count
infDef (Towers 3 count) = 4500 * count
...</pre>
<p>Il reste maintenant à modifier les signatures de nos différentes fonctions de calcul de score :</p>
<pre class="brush: erl">attackPower :: Attacker a => [a] -> Int
attackPower = foldr (\x y -> attack x + y) 0
defensePower :: (Attacker a, Defender d) => Wall -> [d] -> [a] -> Int
...
battle :: (Attacker a, Defender d) => [a]-> [d] -> Wall -> Int
battle att def wall = (attackPower att) - (defensePower wall def att)</pre>
<p>Nous avons effectué ici un simple refactoring.</p>
<p>L’introduction de cette nouvelle unité n’a pas posé de problème et s’est plutôt bien inscrite dans la structure existante, quitte à la raffiner. Maintenant, il a été décidé de rajouter encore une nouvelle unité de protection à la ville, complètement différente cette fois-ci. Il s’agit des protections lourdes aux abords de la ville et qui augmentent, par un pourcentage, la défense globale des troupes dans la ville ; ce pourcentage est fonction du niveau de la protection :</p>
<pre class="brush: erl">data BindingProtections = BindingProtections Int Int
deriving (Show, Read)</pre>
<p>Cette nouvelle unité, comme pour les murs de la ville, constitue une protection qui influe sur le calcul du score des défenseurs de la ville. Il suffirait alors juste de rajouter ce nouveau type dans la signature de nos fonctions de calcul des points de défense :</p>
<pre class="brush: erl">defensePower :: (Attacker a, Defender d) => Wall -> [BindingProtections] -> [d] -> [a] -> Int
...
battle :: (Attacker a, Defender d) => [a] -> [d] -> Wall -> [BindingProtections] -> Int
...</pre>
<p>L’inconvénient immédiat de cette approche est qu’à chaque introduction d’un nouveau type de protection, la signature de nos fonctions augmenterait jusqu’à ne plus en être lisible. Ceci peut se résoudre de deux manières :</p>
<ul>
<li>utiliser la currification pour éclater notre fonction de calcul de la défense en différentes fonctions et les assembler ensuite dans la fonction de calcul de l’issue du combat,</li>
<li>ou définir un nouveau type qui rassemble l’ensemble des protections de la ville influant sur le score des défenseurs et seul ce type changera avec l’ajout de nouvelles protections.</li>
</ul>
<p>J’ai fais le choix, ici, de la seconde solution :</p>
<pre class="brush: erl">data TownProtections = TownProtections Wall [BindingProtections]
applyProtections :: TownProtections -> Int -> Int
applyProtections (TownProtections (Wall r) p) defPower = r + (round $ percentOf (foldr (+) 0 $ map (\x -> count x * level x) p)) * defPower
where
percentOf n = (fromIntegral n :: Float) / 100
count (BindingProtections c _) = c
level (BindingProtections _ l) = l</pre>
<p>Et il suffit alors de remplacer le type <code>Wall</code> par <code>TownProtections</code> dans la signature des fonctions et utiliser <code>applyProtections</code> dans le corps de celles-ci :</p>
<pre class="brush: erl">defensePower :: (Attacker a, Defender d) => TownProtections -> [d] -> [a] -> Int
defensePower (TownProtections (Wall r) []) [] [] = r
defensePower prot def att = applyProtections prot $ (round $ (infantryPercent att) * (infantryPower def) +
(cavalryPercent att) * (cavalryPower def) + (magePercent att) * (magicPower def) +
(artilleryPercent att) * (artilleryPower def))
where
asFloat i = fromIntegral i :: Float
attackingPower = asFloat $ attackPower att
cumulAttack = foldr (\x y -> attack x + y) 0
percentOf n = (asFloat n) / attackingPower
infantryPercent a = percentOf $ cumulAttack $ filter (\x -> kind x == Infantry) a
cavalryPercent a = percentOf $ cumulAttack $ filter (\x -> kind x == Cavalry) a
magePercent a = percentOf $ cumulAttack $ filter (\x -> kind x == Mage) a
artilleryPercent a = percentOf $ cumulAttack $ filter (\x -> kind x == Artillery) a
infantryPower a = asFloat $ foldr (\x y -> y + infDef x) 0 a
cavalryPower a = asFloat $ foldr (\x y -> y + cavDef x) 0 a
magicPower a = asFloat $ foldr (\x y -> y + magDef x) 0 a
artilleryPower a = asFloat $ foldr (\x y -> y + artDef x) 0 a
battle :: (Attacker a, Defender d) => [a] -> [d] -> TownProtections -> Int
battle att def prot = (attackPower att) - (defensePower prot def att)</pre>
<p>Avec l’introduction du nouveau type et, par conséquent, d’une fonction de calcul de score associée à celui-ci, nous avons réussi à maîtriser l’impact de l’ajout de cette nouvelle demande dans le programme.</p>
<p>Finalement, l’évolution d’un programme écrit avec un langage fonctionnel se fait par l’introduction de nouvelles fonctions qui, par un jeu de composition avec l’existant, permet de maîtriser les modifications du code impacté. Vous aurez aussi constaté que le typage permet aussi de mieux structurer et d’aider à la restructuration du code ; il est un support non seulement pour le développeur dans l’expression de ses idées, mais aussi pour le compilateur dans l’analyse du code (en vue de son optimisation éventuelle) et la détection éventuels de problèmes. De plus, par la nature déclarative des langages fonctionnels, les modifications de l’implémentation sont petites et donc là aussi plus facile à maîtriser.</p>Evolution de code avec Haskell (partie 1) : conceptualisationurn:md5:2f28da6147fa86ea870b0e757e5fd1fc2013-04-11T23:24:00+02:002016-09-08T07:50:32+02:00Miguel MoquillonLangagesHaskellprogrammation fonctionnelle<p>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é.</p>
<p>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.</p> <p>Le programme sur lequel nous allons porter notre attention est un calculateur du résultat d’une bataille pour le jeu King’s Empire. Ce dernier est un jeu MMORPG (Massively Multiplayer Online Role Playing Game) pour smartphone et tablette disponible aussi bien sur iOS que sur Androïd.</p>
<p>Lorsque nous devons développer un logiciel, notre première approche est d’identifier les concepts sous-jacents au domaine adressé par l’application ; ici un monde fantastico-médiéval. Ces concepts peuvent être concrets (un paladin, un arsenal, …) comme abstraits (une bataille, une alliance, …). Selon l’approche de programmation que nous allons adopter, par le biais du langage, ces concepts seront représentés dans le programme sous une forme différente ; en POO, les concepts prendront la forme d’objets tandis qu’en programmation fonctionnelle, ils seront représentés par des fonctions. Chacune de ces formes permet l’abstraction, la composition et l’extensibilité des concepts implémentés, propriétés nécessaires à l’évolution du logiciel face non seulement aux demandes fonctionnelles mais aussi aux mutations technologiques. Dans ce billet, nous allons porter notre attention sur la conceptualisation ; l’évolution du code sera le sujet d’un billet ultérieur.</p>
<p>Le paradigme de programmation adopté ici est celui fonctionnel. Notre paladin pourrait être représentée par une fonction qui renverrait ces propriétés en attaque et en défense (vis à vis de l’infanterie, de la cavalerie, de la magie et de l’artillerie) :</p>
<pre class="brush: erl">import Data.Map as M (fromList, map, (!))
paladin = M.fromList [("attack", 60), ("infDef", 45), ("cavDef", 50), ("magDef", 90), ("artDef", 40)]</pre>
<p>Évidemment, dans le jeu, nous utilisons surtout <em>des</em> paladins, <em>des</em> écuyers, etc. Le concept manipulé n’est donc pas en fait le paladin, mais les paladins (le symbole <code>$</code> permet de remplacer les parenthèses englobant ce qui suit et qui est utilisé pour indiquer l’ordre d’exécution de droite à gauche) :</p>
<pre class="brush: erl">import Data.Map as M (fromList, map, (!))
paladins count = M.map (*count) $ M.fromList [("attack", 60), ("infDef", 45), ("cavDef", 50), ("magDef", 90), ("artDef", 40)]</pre>
<p>Il ne resterait alors plus qu’à rajouter les fonctions d’accès aux propriétés des différentes unités militaires:</p>
<pre class="brush: erl">attack units = units ! "attack"
infDef units = units ! "infDef"
...</pre>
<p>Cette approche est probablement celle que l’on adopterait avec un langage à typage dynamique comme, par exemple, Clojure. Or, Haskell permet de jouer avec les types, pourquoi ne pas en profiter pour représenter nos troupes comme formant un seul type :</p>
<pre class="brush: erl">data Troop = Swordmen Int | Scouts Int | Crossbowmen Int | Squires Int | Templars Int |
CavalryArchers Int | Paladins Int | RoyalKnights Int | Rams Int | ArcaneMages Int |
BattleMages Int | HolyClergies Int | IronGolems Int
deriving (Show, Read)</pre>
<p>Le type <code>Troop</code> définit l’ensemble de ses valeurs possibles, chacune générée par un constructeur (une fonction) qui se trouve ici paramétré par un entier qui, dans notre programme, représentera le nombre d’unités. Pour obtenir des paladins, il suffira juste d’appeler le constructeur idoine avec, en argument, le nombre de paladins que l’on a. Vous remarquerez que l’on a pas défini les propriétés de chacune des valeurs du type. Elles pourront l’être sous la forme d’un jeu de fonctions :</p>
<pre class="brush: erl">attack :: Troop -> Int
attack (Paladins count) = 60 * count
attack (RoyalKnights count) = 100 * count
...</pre>
<p>Ou :</p>
<pre class="brush: erl">attack :: Troop -> Int
attack t = case t of
(Paladins count) -> 60 * count
(RoyalKnights count) -> 100 * count
...</pre>
<p>Il pourrait être sympathique de pouvoir rassembler ces propriétés dans un même moule et tisser une relation entre lui et notre type représentant les troupes militaires. Justement Haskell supporte les classes de types. Une classe de types est un type de types ; c’est-à-dire qu’elle définit l’ensemble des fonctions que les types doivent implémenter pour appartenir à la classe. Or, justement, ces caractéristiques peuvent ne pas être l’apanage de nos troupes mais caractériser toute unité de combat quel qu’elle soit. Pourquoi ne pas représenter alors les caractéristiques de nos troupes sous une telle forme et dans laquelle chacune des propriétés est définie par une fonction, et faire en sorte que notre type <code>Troop</code> soit un élément de cette classe, autrement dit donne corps à chacune des propriétés de combat :</p>
<pre class="brush: erl">data FighterClass = Infantry | Cavalry | Artillery | Mage
deriving (Eq)
class Fighter a where
kind :: a -> FighterClass
attack :: a -> Int
infDef :: a -> Int
cavDef :: a -> Int
magDef :: a -> Int
artDef :: a -> Int
instance Fighter Troop where
attack (Swordmen count) = 40 * count
attack (Scouts count) = 10 * count
...
infDef (Swordmen count) = 15 * count
infDef (Scouts count) = 10 * count
...</pre>
<p>Maintenant, il reste à définir l’attaque et la défense d’une armée selon les règles du jeu King’s Empire et en se basant sur notre classe de types :</p>
<pre class="brush: erl">attackPower :: Fighter a => [a] -> Int
attackPower = foldr (\x y -> attack x + y) 0
defensePower :: Fighter a => Wall -> [a] -> [a] -> Int
defensePower (Wall r) _ [] = r
defensePower (Wall r) def att = r + (round $ (infantryPercent att) * (infantryPower def) +
(cavalryPercent att) * (cavalryPower def) + (magePercent att) * (magicPower def) +
(artilleryPercent att) * (artilleryPower def))
where
asFloat i = fromIntegral i :: Float
attackingPower = asFloat $ attackPower att
cumulAttack = foldr (\x y -> attack x + y) 0
percentOf n = (asFloat n) / attackingPower
infantryPercent a = percentOf $ cumulAttack $ filter (\x -> kind x == Infantry) a
cavalryPercent a = percentOf $ cumulAttack $ filter (\x -> kind x == Cavalry) a
magePercent a = percentOf $ cumulAttack $ filter (\x -> kind x == Mage) a
artilleryPercent a = percentOf $ cumulAttack $ filter (\x -> kind x == Artillery) a
infantryPower a = asFloat $ foldr (\x y -> y + infDef x) 0 a
cavalryPower a = asFloat $ foldr (\x y -> y + cavDef x) 0 a
magicPower a = asFloat $ foldr (\x y -> y + magDef x) 0 a
artilleryPower a = asFloat $ foldr (\x y -> y + artDef x) 0 a</pre>
<p>Le calcul des points des armées s’appuie sur la propriété que les unités qui les composent satisfont la classe de type <code>Fighter</code>.
Le calcul des points de défenses de l’armée attaquée est plus complexe car d’une part ils s’appuient sur les capacités défensives de la ville (ici les remparts qui entourent la ville et représentée par le type <code>Wall</code>) et d’autre part ils sont conditionnés par les capacités offensives de l’agresseur. Ainsi, par exemple, les points défensifs de l’infanterie du défenseur sont proportionnés aux points d’attaques de l’infanterie présente dans l’armée ennemie. De ces calculs, nous pouvons alors déterminer laquelle des deux armées sera victorieuse :</p>
<pre class="brush: erl">-- Runs the battle between two armies, whose one is behind the town's protections.
-- It returns the ratio between the attack power and the defense power:
-- a positive value means the attacker wins whereas a negative value means the defenser wins.
-- More the absolute ratio is high, more damages the lost army are important.
battle :: Fighter a => [a]-> [a] -> Wall -> Int
battle att def wall = (attackPower att) - (defensePower wall def att)</pre>
<p>Nous avons donc représenté les différentes unités militaires du jeux King’s Empire comme étant des valeurs du type <code>Troop</code> qui est une instance de la classe de types <code>Fighter</code>. Nous aurions pu très bien, par soucis de simplicité, représenter les troupes comme un simple constructeur des différentes propriétés de combat et définir chacune des unités par le biais d’une fonction :</p>
<pre class="brush: erl">data TroopType = Infantry | Cavalry | Artillery | Mage
deriving (Eq, Read, Show)
data Troop = Troop { kind::TroopType, attack::Int, infDef::Int, cavDef::Int, magDef::Int, artDef::Int }
deriving (Show, Read)
data TroopUnit = Swordmen | Scouts | Crossbowmen | Squires | Templars | CavalryArchers |
Paladins | RoyalKnights | Rams | ArcaneMages | BattleMages | HolyClergies | IronGolems
deriving (Show, Read)
troopOf :: Int -> TroopUnit -> Troop
troopOf n Paladins = Troop { kind = Cavalry, attack = n * 60, infDef = n * 45, cavDef = n * 50,
magDef = n * 90, artDef = n * 40 }
...</pre>
<p>Dans notre solution, les propriétés sont rassemblées, au sein de la classe de type, par leur nature (attaque, défense en infanterie, etc.) quelque soit l’unité militaire. Dans cette dernière solution, elles sont rassemblées par unité. L’avantage est que les propriétés de chaque unité ne sont plus diluées dans différentes fonctions mais, au contraire, rassemblées au sein d’une seule et leur modification reste alors plus simple et donc moins sujette à l’erreur.</p>
<p>Avec ce billet, vous avez pu constater que les constructions dans un langage fonctionnel peuvent être riches et offrir ainsi plusieurs manières de représenter les concepts d’une application. L’enjeu sera alors d’identifier celle qui facilitera plus facilement l’évolution du code, et en cette matière les techniques de l’Agilité, comme le TDD, peuvent fortement nous aider.</p>Un benchmark sur le tri rapide dans 5 langagesurn:md5:99ceb5a4e02711414f290c01ff9a765b2012-11-22T23:10:00+01:002016-09-08T07:55:39+02:00Miguel MoquillonLangagesCGoHaskellJavaScala<p>L’article de James Roper sur <a href="https://jazzy.id.au/default/2012/10/16/benchmarking_scala_against_java.html" hreflang="en" title="Performances de Scala et de Java">les performances de Scala et de Java via l’exemple du tri rapide</a> m’a donnée l’idée, juste pour amusement, de réaliser le même benchmark mais avec 5 langages différents : C, Go, Java, Scala et Haskell ; on y retrouve donc ici à la fois des langages à orientation impérative et d’autres à orientation fonctionnelle. L’implémentation de l’algorithme est celui utilisé dans son article mais déclinée selon deux axes pour les langages de nature impérative : un axe plus classique dans lequel la récursivité est utilisée, et un autre bien moins traditionnel dans lequel la récursivité est, au contraire, évitée.</p> <p>Pour commencer, le benchmark a été réalisé sur deux machines différentes :</p>
<ul>
<li>un portable Toshiba Satellite Pro A40 équipé d’un processeur Pentium 4 Mobile 2,9GHz, de 2Go de RAM, d’un disque dur 7200TPM de 100Go, le tout tournant sous GNU/Linux ArchLinux ;</li>
<li>et un portable Acer 8942G doté d’un processeur i7 720QM (1.6GHz), de 6Go de RAM et d’un disque SSD Intel de 80Go, le tout tournant sous GNU/Linux KUbuntu 12.04.</li>
</ul>
<p>Afin que les implémentations avec et sans récursivité soient suffisamment comparables, j’ai pris le choix de partir avec le même code de tri des sous listes autour du pivot. Le choix fait pour le second axe a été de remplacer la récursivité simplement par une boucle sur une pile dans laquelle les indexes des seuils de chaque sous-liste sont empilés puis dépilés au fur et à mesure du traitement. Vous trouverez les programmes dans un de mes <a href="https://gitorious.org/quicksort-benchmark/quicksort-benchmark/trees/master" hreflang="en" title="Dépôt Git sur Gitorious">dépôts Git sur Gitorious</a>.</p>
<p>Pour chaque langage, le test a été exécuté plusieurs fois jusqu’à obtenir une déviation la plus petite possible et une certaine stabilité dans les résultats.</p>
<p>Le test a d’abord été réalisé avec le langage C et ceci selon les deux axes présentés ci-dessus. Les mesures du programme en C sont là pour établir une base de comparaison pour les différents langages. Il a été compilé classiquement avec l’option -O2 de GCC. L’outil de build utilisé est ici le traditionnel Make.</p>
<p>Toshiba Satellite Pro A40, GCC 4.7.2 :</p>
<pre class="brush: plain">Recursive quicksort -> mean time over 100 samples: 14ms with as standard deviation: 4.690416ms
Non-recursive quicksort -> mean time over 100 samples: 12ms with as standard deviation: 1.732051ms</pre>
<p>Acer 8942G, GCC 4.6.3 :</p>
<pre class="brush: plain">Recursive quicksort -> mean time over 100 samples: 9ms with as standard deviation: 0.000000ms
Non-recursive quicksort -> mean time over 100 samples: 8ms with as standard deviation: 0.000000ms</pre>
<p>Compte tenu de la déviation, sur le vieux PC portable, les deux versions du tri rapide ont des résultats proches. Quoiqu’il en soit, sur les deux configurations, la version du tri rapide sans récursivité présente de meilleurs performances, et ceci bien que le compilateur supporte le <em>tail-recursion</em> (optimisation des appels récursifs en queue, c’est-à-dire en fin de fonction). Je constate aussi que le tri non récursif est moins sujet à la variance des 100 mesures sur le Toshiba, confirmé à chaque exécution du test. Chose intéressante, je n’obtiens pas de variances entre les mesures dans le test sur le portable PC récent.</p>
<p>Quitte à tester le programme en C, faisons le aussi avec le même programme mais en Go, le langage développé par des concepteurs même du C (Rob Pike et Ken Thompson). Évidemment, le langage étant récent, je ne m’attend pas à une grande surprise. A noter que j’ai pris prétexte du benchmark pour essayer ce langage.</p>
<p>Toshiba Satellite Pro A40, Go 1 :</p>
<pre class="brush: plain">Recursive quicksort -> mean time over 100 samples: 26ms with as standard deviation: 5.247003228167484ms
Non recursive quicksort -> mean time over 100 samples: 29ms with as standard deviation: 2.783526440973752ms</pre>
<p>Acer 8942G, Go 1 :</p>
<pre class="brush: plain">Recursive quicksort -> mean time over 100 samples: 13ms with as standard deviation: 0.5799725093485035ms
Non recursive quicksort -> mean time over 100 samples: 14ms with as standard deviation: 0.43859390055038383ms</pre>
<p>Première remarque, compte tenu de la déviation sur le vieux PC portable, le temps d’exécution des deux versions sont là aussi proches. Mais, au contraire de la version C, c’est le tri récursif qui présente de meilleurs performances. Ceci ne doit pas surprendre car le compilateur Go de Google implémente aussi le <em>tail-recursion</em> mais de façon, semble t’il, plus efficace que le compilateur GCC. Au vue du résultat, finalement, il n’y a pas lieu de se triturer les méninges pour remplacer la récursivité lorsque celle-ci est naturelle à l’algorithme et qu’elle est en queue d’une fonction. Seconde remarque, les performances du programme en Go sont derrières celles de celui en C mais, au regard de la jeunesse du langage, elles sont plutôt bonnes et laissent présager de belles surprises à l’avenir. De plus, la déviation standard est relativement faible, ce qui démontre une certaine stabilité dans l’exécution du programme.</p>
<p>Maintenant, passons à Java (version 1.6.0 ici) qui lui ne supporte pas l’optimisation <em>tail-recursion</em>. La différence cette fois-ci avec les deux précédents langages est qu’en Java nous pouvons exprimer le tri sur des éléments de plus haute abstraction. En l’occurrence, les fonctions de tri sont définies pour des éléments qui satisfont l’interface <code>java.lang.Comparable</code>. L’outil de build utilisé ici est Maven (version 3.0.4) et le plugin <code>exec-maven-plugin</code> est utilisé pour exécuter le programme une fois celui-ci compilé.</p>
<p>Toshiba Satellite Pro A40, Java OpenJDK 1.6.0 Update 24 :</p>
<pre class="brush: plain">Recursive quicksort -> mean time over 100 samples: 184ms with as standard deviation: 23.958297101421877ms
Non-recursive quicksort -> mean time over 100 samples: 201ms with as standard deviation: 30.56141357987225ms</pre>
<p>Acer 8942G, Java Sun HotSpot 1.6.0 Update 29:</p>
<pre class="brush: plain">Recursive quicksort -> mean time over 100 samples: 17ms with as standard deviation: 3.3166247903554ms
Non-recursive quicksort -> mean time over 100 samples: 21ms with as standard deviation: 3.1622776601683795ms</pre>
<p>Les résultats du programme en Java m’ont surpris. En effet, la version du tri non-récursif est bien moins performante que celle récursive alors que le compilateur Java ne supporte pas le <em>tail-recursion</em>. Une explication peut être trouvée à la lecture du bytecode de chacune des versions du tri. En effet, on remarque un plus grand nombre d’appels de type <code>invokeinterface</code> et <code>invokestatic</code> dans le tri non-récursif ; par exemple, j’ai compté 21 <code>invokeinterface</code> contre 10 dans le tri récursif. De plus, les deux seuls appels <code>invokestatic</code> dans la version récursive correspond justement aux deux appels récursifs du tri. En remplaçant l’utilisation d’une instance <code>Deque</code> par un tableau pour représenter la pile dans le tri non récursif, on améliore les performances pour s’approcher de celles de la version récursive (18ms en temps moyen avec la même déviation standard). Sinon, vis à vis des mesures, le programme Java présente une variance des mesures plus importante que sa contre-partie en C ou en Go, mais reste toutefois modérée.</p>
<p>Passons maintenant au programme en Scala. Ici, SBT est utilisé comme outil de build. Comme avec Java, le langage nous permet d’exprimer une plus grande abstraction dans la définition de la fonction de tri ; elle peut s’appliquer sur toute liste ou tableau d’éléments qui satisfont le trait <code>Ordered</code>. Les traits apportent une plus forte expressivité que les interfaces dans Java. Ceux-ci, couplés avec la généricité, permettent de représenter des types algébriques avec lesquels différent typage de second ordre contraint peut être défini, dont celui F-Bound caractéristique de la programmation objet. On obtient alors une plus haute abstraction. Contrairement avec les autres langages, ce n’est ici plus les aspects récursif et non-récursif qui m’ont intéressé mais les approches impératives et fonctionnelles que supportent le langage Scala.</p>
<p>Toshiba Satellite Pro A40, Scala 2.9.2 :</p>
<pre class="brush: plain">Imperative quicksort -> mean time over 100 samples: 489ms with as standard deviation: 38.98717737923585ms
Functional quicksort -> mean time over 100 samples: 1093ms with as standard deviation: 22.715633383201094ms</pre>
<p>Acer 8942G, Scala 2.9.1 :</p>
<pre class="brush: plain">Imperative quicksort -> mean time over 100 samples: 83ms with as standard deviation: 11.704699910719626ms
Functional quicksort -> mean time over 100 samples: 128ms with as standard deviation: 22.24859546128699ms</pre>
<p>Les deux formes utilisent la récursion et le compilateur Scala supporte l’optimisation <em>tail-recursion</em>. L’approche fonctionnelle utilise l’aptitude du langage à exprimer clairement l’idée sous-jacente au tri rapide mais avec toutefois des performances plutôt déplorable. Quoiqu’il en soit, nous remarquons que le programme équivalent à son pendant en Java est bien moins performant. A côté de ceci, les variances entre les mesures sont fortes.</p>
<p>Comparons maintenant le programme en Scala sous sa forme fonctionnelle avec celui équivalent mais en Haskell. Celui-ci est un langage fonctionnel qui offre un niveau abstraction inégalé comparé aux langages impératifs. Ici, une bibliothèque dédiée aux tests de performances, Criterion, est utilisée et permet d’avoir des chiffres bien plus pertinents. Parce que la génération aléatoire de la liste à trier est une des fonctions avec la laquelle est composée l’appel à la fonction de tri, il a été nécessaire de mesurer son temps de génération afin d’en déduire celui du tri. Il a fallu faire aussi avec les propriétés paresseuses du langage : tant que les éléments de la liste ne sont pas accédés, ceux-ci ne sont pas générés. L’utilisation de la fonction <code>nf</code> de Criterion pour mesurer les performances du tri permet justement de forcer le tri de la liste et donc aussi sa génération.</p>
<p>Toshiba Satellite Pro A40, GHC 7.6.1 :</p>
<pre class="brush: plain">warming up
estimating clock resolution...
mean is 5.211127 us (160001 iterations)
found 286073 outliers among 159999 samples (178.8%)
147056 (91.9%) low severe
139017 (86.9%) high severe
estimating cost of a clock call...
mean is 1.061649 us (49 iterations)
benchmarking random list generation
collecting 100 samples, 1 iterations each, in estimated 33.02100 s
mean: 2.250232 ms, lb 2.241430 ms, ub 2.258059 ms, ci 0.950
std dev: 42.47253 us, lb 36.00341 us, ub 50.80494 us, ci 0.950
found 5 outliers among 100 samples (5.0%)
4 (4.0%) low mild
variance introduced by outliers: 11.368%
variance is moderately inflated by outliers
benchmarking quicksort
collecting 100 samples, 1 iterations each, in estimated 69.37261 s
mean: 711.1199 ms, lb 704.3110 ms, ub 722.0640 ms, ci 0.950
std dev: 43.62053 ms, lb 28.08513 ms, ub 67.92878 ms, ci 0.950
found 2 outliers among 100 samples (2.0%)
2 (2.0%) high severe
variance introduced by outliers: 58.505%
variance is severely inflated by outliers</pre>
<p>Acer 8942G, GHC 7.4.1 :</p>
<pre class="brush: plain">warming up
estimating clock resolution...
mean is 1.646377 us (320001 iterations)
found 2196 outliers among 319999 samples (0.7%)
1636 (0.5%) high severe
estimating cost of a clock call...
mean is 43.42828 ns (11 iterations)
found 1 outliers among 11 samples (9.1%)
1 (9.1%) high severe
benchmarking random list generation
collecting 100 samples, 1 iterations each, in estimated 18.22550 s
mean: 1.115150 ms, lb 1.078057 ms, ub 1.169435 ms, ci 0.950
std dev: 227.4210 us, lb 168.8725 us, ub 315.9030 us, ci 0.950
found 8 outliers among 100 samples (8.0%)
4 (4.0%) high mild
4 (4.0%) high severe
variance introduced by outliers: 94.661%
variance is severely inflated by outliers
benchmarking quicksort
collecting 100 samples, 1 iterations each, in estimated 27.53069 s
mean: 266.7308 ms, lb 265.7461 ms, ub 267.7269 ms, ci 0.950
std dev: 5.064837 ms, lb 4.531545 ms, ub 5.731438 ms, ci 0.950
variance introduced by outliers: 12.268%
variance is moderately inflated by outliers</pre>
<p>Le tri est réalisé en 709ms environ avec la vieille machine tandis que celui-ci est bouclé en 266ms sur la machine plus récente. On a ici des résultats mitigés : bien plus performant sur le portable Toshiba que le programme en Scala, le programme en Haskell l’est moins sur le portable Acer. Toutefois, si la variance est modérée sur la machine récente, elle est bien plus importante sur la plus vieille machine. Globalement, le programme en Haskell est moins performant que ce à quoi je m’attendais, et il faudra probablement user de techniques particulières pour atteindre de meilleures performances (mais à quel prix ? Et avec quelle complexité ?)</p>
<p>Si vous les programmes du benchmark vous intéresse, vous les trouverez dans un de mes <a href="https://gitorious.org/quicksort-benchmark/quicksort-benchmark/trees/master" hreflang="en" title="Dépôt Git sur Gitorious">dépôts Git sur Gitorious</a>. Amusez-vous bien …</p>