points de vue - Mot-clé - foncteurles déambulations d'un codeur2023-06-18T04:35:57+02:00Miguel Moquillonurn:md5:2d53372d0fa8f28ff50e71462648e0b8DotclearFoncteurs, 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>