Il y a 11 ans -
Temps de lecture 7 minutes
Les types monadiques de Scala – Le type Option
Cet article est le premier d’une série dans laquelle nous étudierons les types dit monadiques fournis par Scala et couramment utilisés lors de développements d’applications. Un type monadique est un type de donnée répondant à certaines lois et généralement caractérisé dans Scala par la présence des méthodes map
et flatMap
, que nous aborderons ici même.
Cet article d’introduction aux monades nous montrera comment l’utilisation de ces types permettent de produire du code plus sûr et évolutif. Il sera suivi par une série d’articles ayant pour objectif de démystifier ce concept qui provient du monde de la programmation fonctionnelle (et plus particulièrement de Haskell) et débarque depuis peu dans le monde Java (grâce notamment au langage Scala ou à des frameworks tels que FunctionalJava). Ce sera aussi l’occasion de comprendre pourquoi l’expression « flatMap that shit ! » est utilisée avec autant de ferveur.
NB. pour les puristes, j’ai fait un abus de langage dans cet article en parlant de type Option
. C’est bien évidemment un type constructeur et un non type concret.
Définition du type Option
Commençons notre exploration par un des types le plus souvent utilisés à savoir le type Option
. Une Option[A]
est un type abstrait générique permettant de caractériser la présence ou l’absence de valeur via deux sous types qui sont Some[A]
ou None
. Un exemple classique d’utilisation du type Option
est le suivant :
scala> def divide(x:Double, y:Double) = if (y == 0) None else Some(x/y) divide: (x: Double, y: Double)Option[Double] scala> divide(4, 2) res0: Option[Double] = Some(2.0) scala> divide(4, 0) res1: Option[Double] = None
Le type Option
permet de traiter de façon particulièrement élégante certains cas limite de fonctions pour lesquels aucun résultat n’existe. Un autre exemple d’utilisation de l’Option
est la dénotation de l’absence de valeur (absence de donnée dans un formulaire web, représentation d’un champs nullable en base etc.). Dans ce cas, l’absence de valeur est directement traduite par le type de la donnée.
Nous pouvons voir que le type de retour de divide(4, 2)
est Option[Double]
. Ce type a été inféré par le compilateur en fonction des types de x
et de y
. Notez bien que Option
est un type abstrait et qu’il ne peut être construit directement mais doit nécessairement passer par un de ses deux sous types Some
et None
.
Un autre exemple d’utilisation du type Option
est une méthode prenant en paramètre un identifiant unique et retournant une valeur unique :
def findById(id:Long):Option[User] = { … }
Dans cet exemple, la fonction nous retournera un objet de type Some[User]
si l’identifiant correspond à un utilisateur ou bien None
dans le cas contraire.
Traitement des Options
Nous avons vu comment créer une Option
, il est temps maintenant de comprendre comment l’exploiter.
Reprenons l’exemple de notre méthode findById
:
val userOption = findById(1)
Le type de userOption
est Option[User]
et peut donc être soit Some[User]
soit None
. Une première approche serait d’utiliser les méthodes get
et isDefined
:
if (userOption.isDefined) { userOption.get.toString } else { "no result" }
Cette approche impérative fonctionne, mais elle n’apporte pas de valeur ajoutée par rapport à une approche utilisant des comparaisons avec null
. Nous pouvons faire beaucoup mieux ! L’API Scala nous propose la méthode getOrElse
retournant la valeur contenue dans l’Option
ou bien l’évaluation de l’expression passée en paramètre. Si nous appliquons directement getOrElse
sur userOption
, le résultat attendu sera un User
et non pas la représentation de l’utilisateur sous forme de String
. Nous devons donc préalablement transformer l’Option[User]
en Option[String]
:
userOption.map( u => u.toString ).getOrElse("no result")
La méthode map
d’une Option[A]
prend une fonction de type A => B
et retourne une Option[B]
. Dans notre cas, elle prend un User
et retourne une String
. Implicitement, la méthode getOrElse
est, comme map
, une fonction d’ordre supérieure prenant en argument une expression qui sera évaluée uniquement si userOption
est None
. Elle retourne une valeur du type String
.
Comme vous pouvez vous en douter, il est possible de chaîner des transformations en appliquant successivement des map
avant de récupérer la valeur finale avec un getOrElse
. Imaginons que nous souhaitons récupérer la représentation JSON de notre utilisateur :
userOption.map( _.toJson ).map( _.toString ).getOrElse("{}")
Notez ici l’utilisation de _ qui est un sucre syntaxique désignant le paramètre de la fonction passée à map
(et donc équivalent à x => x.toJson
pour la première fonction). Dans le cas où la valeur de userOption
serait None
, les deux transformations ne seront pas appliquées et le getOrElse
renverra "{}"
.
Composition des Options
Nous venons de voir comment transformer le contenu d’une Option
avant d’en extraire la valeur avec getOrElse
. Comment traiter le cas où nous souhaiterions récupérer les valeurs de deux Option
et en extraire les valeurs ?
val firstOption:Option[Int] = getIntOption(1) val secondOption:Option[Int] = getIntOption(2) firstOption.flatMap { first => secondOption.map { second => first + second } }
Dans cet exemple, getIntOption
est censé prendre en argument un entier et retourner une Option
contenant un entier (ou None
si pas de résultat). Nous utilisons ensuite une combinaison d’appel à map
et à flatMap
pour additionner les deux valeurs et retourner une Option
contenant la somme des valeurs ou None
, si l’un des deux appels à getIntOption
nous a retourné None
.
Nous avons déjà rencontré map
auparavant. Cette méthode de Option[A]
prend en argument une fonction f: A => B
et retourne une Option[B]
. La méthode flatMap
de Option[A]
en revanche est nouvelle. Elle prend en argument une fonction f: A => Option[B]
et retourne une Option[B]
.
Si on regarde un peu plus en détail la fonction passée en argument de flatMap
:
{ first => secondOption.map { second => first + second } }
Cette fonction prend en argument first
qui est de type Int
et retourne le résultat de map(Int => Int)
sur une Option[Int]
, qui sera donc de type Option[Int]
. Du coup, notre méthode flatMap
prend en argument une fonction f: Int => Option[Int]
et retourne donc une Option[Int]
contenant le résultat de l’évaluation de first + second
. On voit ici toute la puissance de l’approche fonctionnelle par rapport à une approche impérative qui aurait imposé une imbrication de if
ou d’opérateurs ternaires pour obtenir le même résultat.
Pour simplifier la lecture, Scala nous propose une écriture différente mais amenant au même résultat en utilisant les for comprehensions. Commençons par indenter différemment l’expression calculant la somme des Option
:
firstOption.flatMap { first => secondOption.map { second => first + second } }
Que l’on traduira à l’aide d’une for comprehension :
for { first <- firstOption second <- secondOption } yield (first + second)
La for comprehension est automatiquement traduite par le compilateur Scala en expression utilisant une combinaison de map
et de flatMap
.
Pattern matching
Nous avons vu qu’il était possible de récupérer la valeur d’une Option
à l’aide de la méthode getOrElse
. Il est aussi possible de faire du pattern matching sur une option pour exploiter son contenu (ou son absence de contenu) :
getUser(1) match { case None => BadRequest("invalid user") case Some(user) => Ok("Welcome %s %s".format(user.firstname, user.lastname)) }
le résultat de cette évaluation sera donc un objet de type BadRequest
ou Ok
en fonction de l’Option
.
Conclusion
Nous venons dans cet article de voir comment utiliser notre première monade. L’adoption du type Option
dans nos développements permet de répondre d’une façon particulièrement élégante au problème d’absence de valeur qui est généralement traduite par l’utilisation d’une référence null en programmation Java classique. En utilisant le type Option
, non seulement nous nous prémunissons contre les erreurs de type NullPointerException
, mais nous pouvons aussi compter sur le compilateur pour nous imposer de traiter le cas None
dénotant l’absence de valeur. Une utilisation adéquate du type Option
permettra donc d’éviter la remonté d’exceptions au runtime.
Le prochain article de cette série introduira une autre monade assez proche de l’Option
, le type Either
. Dans cet article et dans les suivants, nous tenterons d’améliorer notre compréhension des monades, pour aboutir à une définition formelle de ce concept.
Un peu de teasing pour finir, François Sarradin publiera prochainement un article sur l’implémentation de monades en Java. Il montrera ainsi qu’il est possible d’utiliser des monades (comme l’Option
) dans vos applications Java.
Commentaire
2 réponses pour " Les types monadiques de Scala – Le type Option "
Published by Tanjona , Il y a 11 ans
Tres bon article avec des utilisations pratique, il est assez difficile de retrouver des ressources en Français.
ma seul remarque serait peut être comment utiliser les options sur des valeur null (venant d’Hibernate par exemple)
>val test = null
>Option(test) == None // true
Sinon il y a net.liftweb.common.Box dans Le framework Liftweb qui gere les exceptions en plus des valeurs Null vraiment pratique dans les conversions. ou encore mieux « tryo » qui permet de gérer tres facilement les Box « Failure »
http://www.assembla.com/spaces/liftweb/wiki/Box
Merci pour l’article !
Published by David Galichet , Il y a 11 ans
Merci pour ce commentaire ;-)
effectivement je suis passé à côté de l’Option(null) == None qui peut être bien pratique lorsque l’on travaille avec des librairies Java utilisant une null reference pour indiquer l’absence de valeur.
Published by Hamdane , Il y a 6 ans
Bon article,
Merci David.