Comprendre les monades avec Scala

Comprendre les monades avec Scala

Si vous vous intéressez à la programmation fonctionnelle, vous avez sûrement déjà entendu parler des monades et peut-être les avez-vous utilisées dans le savoir.

Une monade est un type paramétrique M prenant en type paramétré T et définissant 2 opérations : flatMap (aussi appelé bind) et unit (aussi appelé return). Ces 2 opérations doivent satisfaire à certaines lois pour que le type M soit effectivement considéré comme une monade.

Définition d’une monade

En Scala, on peut écrire une monade comme suit :

trait M[T] {
  def flatMap[U](f: T => M[U]): M[U]
  def unit[T](x: T): M[T]
}

Avant d’entrer dans le détail, voyons quel est le but de ces 2 fonctions.

flatMap

La fonction flatMap prend un type arbitraire U et une fonction qui mappe un type T vers une monade de U et retourne une monade M appliquée à U.

unit

La fonction unit prend un élément de type T en paramètre et retourne une instance de la monade M[T]

Exemples de monades en Scala

  • List est une monade où unit(x) = List(x)
  • Set est une monade où unit(x) = Set(x)
  • Option est une monade où unit(x) = Some(x)

Les méthodes unit des ces monades ont des noms qui diffèrent, cependant elles implémentent toutes flatMap en conservant ce nom.

map

La méthode map des monades peut être définie en combinant unit et flatMap :

m map f == m flatMap (x => unit(f(x)))

Si on prend la monade List, les 2 instructions suivantes sont équivalentes :

val l = List(1) 
l.map(x => x * 2)
l.flatMap(x => List(x * 2))

L’output sera

l: List[Int] = List(1)
res0: List[Int] = List(2)
res1: List[Int] = List(2)

Lois des monades

Pour pouvoir être considéré comme une monade, un type doit satisfaire 3 lois :

Associativité

La fonction flatMap doit pouvoir être enchainée quelque soit le placement des parenthèses :

m flatMap f flatMap g == m flatMap (x => f(x) flatMap g)

Il est plus simple d’illustrer la notion d’associativité avec des monoides (des monades qui n’implémentent pas flatMap). Par exemple, avec des integer x, y et z :

x + (y + z) == (x + y) + z

L’associativité est respectée car les 2 formulations renvoient la même valeur.

Composition neutre par return à gauche

Cette loi spécifie que si l’on appelle flatMap sur unit(x) en appliquant f, on doit obtenir f(x) :

unit(x) flatMap f == f(x)

Composition neutre par return à droite

La dernière loi dicte que si l’on applique une unit à flatMap d’une monade m le résultat doit être m :

m flatMap unit = m

Vérification de ces lois sur List

Return à gauche

On souhaite vérifier que unit(x) flatMap f == f(x)

On va écrire une fonction f qui multiplie son paramètre par 2 :

def f(x:Int): List[Int] = List(x * 2)
>f: f[](val x: Int) => List[Int]

Ensuite on va créer une liste qui contient seulement un élément :

val l = List(1)
>l: List[Int] = List(1)

Et on écrit les instructions sensées être équivalentes :

l flatMap f
f(1)

On obtient en sortie

res2: List[Int] = List(2)
res3: List[Int] = List(2)

Le return à gauche est donc vérifié. Passons à la loi suivante.

Return à droite

Pour le return à droite, on veut vérifier que m flatMap unit == m

On réutiliser notre liste l qui contient un seul élément (la valeur 1) :

l.flatMap(List(_))

On obtient en sortie une liste identique à l :

res4: List[Int] = List(1)

Associativité

Pour tester que m flatMap f flatMap g == m flatMap (x => f(x) flatMap g) (ou autre placement de parenthèses), on va créer la fonction g qui multiplie son paramètre par 3 :

def g(x:Int): List[Int] = List(x * 3)

Puis on va tester que l’on obtient bien le même résultat, quelque soit l’ordre des parenthèses :

l flatMap f flatMap g
l flatMap f flatMap(x => g(x))
l flatMap(x => f(x)) flatMap g

La sortie est :

res5: List[Int] = List(6)
res6: List[Int] = List(6)
res7: List[Int] = List(6)

La loi d’associativité est donc bien respectée.

Conclusion

Les monades sont présentes partout en programmation fonctionnelle et leur compréhension me semble importante durant l’apprentissage de ce paradigme.

Ce concept ouvre la voie à l’implémentation des méthodes flatMap, map, foldLeft, foldRight etc.. que je couvrirai dans de futurs articles.

Retrouvez le script utilisé dans son intégralité sur ce gist : https://gist.github.com/alexandrelanglais/6a44c07881d842b03949d31cffd29837

Laisser un commentaire