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.

Publié par

Publié par David Galichet

David a commencé sa carrière en 2004 chez Fimasys, éditeur de progiciel dans la finance puis a rejoint Linagora, une SSLL dans laquelle il a passé un an et demi avant de rejoindre Xebia fin 2009. Il s'intéresse tout particulièrement au monde Java, aux technologies J2EE et aux langages qui gravitent autour comme Groovy et Scala. Il a aussi développé un fort intérêt pour les plateformes d'intégration ainsi que pour les problématiques de production. David a aussi un réel intérêt pour les méthodologies agiles, qu'il a pu appliquer en tant que Scrum Master dans ses différentes missions. Enfin, David participe activement à la revue de presse de Xebia sur des sujets relatifs aux technologies Java et aux méthodologies agiles.

Commentaire

2 réponses pour " Les types monadiques de Scala – Le type Option "

  1. Publié par , Il y a 8 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 !

  2. Publié par , Il y a 8 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.

  3. Publié par , Il y a 3 ans

    Bon article,
    Merci David.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.