De l'intérêt de proposer des exercices de code dans vos équipes : exemple en Scala

Vous arrivez sur un nouveau projet. Pour sa réussite, la maîtrise de tel framework ou de tel langage est un atout. Cependant, vous vous rendez compte que sur cet aspect, votre équipe ne suit pas. Par exemple, Scala est un langage que vous avez la possibilité de rencontrer lors de vos expériences, en particulier dans le monde du Big Data avec Spark (mais pas seulement ^^). Si vous souhaitez apprécier véritablement les capacités du langage Scala, comprendre la programmation fonctionnelle est un atout. Or, même si l'ensemble des langages font des efforts pour intégrer ce style de programmation (eg. Java 8+, JavaScript, Rust...), les cursus d'enseignement et la majorité des projets informatique existants nous ont en général habitués à la programmation impérative, procédurale et orientée objet. Se mettre à la programmation fonctionnelle n’est pas si simple.

Afin de sensibiliser les équipes aux capacités de programmation fonctionnelle de Scala, il m'arrive de proposer aux équipes de développement des exercices à réaliser pour la semaine. Ces exercices sont l’occasion de parler des concepts disponibles à travers le langage. Les équipes sont en général très motivées, voire avides, d'apprendre ainsi ces nouveaux concepts, d'autant que cela se passe dans une bonne humeur générale.

Je vous propose ici de vous donner une série d'exercices liée à la programmation fonctionnelle en Scala, que j'ai l'habitude de donner, après avoir présenté les pré-requis et les règles. Ils sont classés par ordre de difficulté croissante. Nous discuterons par la suite de l'intérêt d'un point de vue général de mettre en place de tels exercices sur les projets.

Pré-requis

Les exercices de cet article sont destinés aux débutants en Scala. Vous devez avoir des connaissances de base avec le langage (en particulier la syntaxe). Néanmoins, certains exercices peuvent être adaptés dans d'autres langages, comme Java 8+, Kotlin, F#, etc.

Si vous voulez d'autres exercices ou des exercices permettant d'aller plus loin avec Scala, allez voir :

  • Scala Exercices proposé par la société 47 Degrees. Ce site propose des exercices pour tout niveau et pour certains frameworks populaires Scala ;
  • 99 Scala Problems. Ce site propose des exercices plus de la catégorie des casses-têtes. Pour chaque exercice, il peut y avoir plusieurs solutions possibles ;
  • Les exercices proposés dans le cadre du MOOC Functional Programming in Scala sur Coursera. Ce MOOC est une introduction à Scala. Les exercices ont une difficulté croissante et nécessitent un travail s'étalant sur la semaine pour certains ;
  • Les exercices proposés dans le livre Functional Programming in Scala de Paul Chuisano et Runar Bjarnason chez Manning Publications. Ce livre est destiné au développeurs ayant une expérience avancée avec Scala. Il permet d'explorer très loin les concepts de la programmation fonctionnelle.

D'une manière générale, la difficulté des exercices que vous allez proposer à vos équipes doit dépendre du niveau général. Si des personnes sont plus avancées, n'hésitez pas à leur demander de participer à la conception des exercices et/ou à l'accompagnement des différents membres de l'équipe dans la résolution.

Règles

Il n'y a pas de règles particulières à respecter, à part de compléter les éléments notés ??? ou de faire en sorte que le code compile. Mais au niveau du code, vous pouvez très bien par exemple utiliser la programmation impérative "pour voir ce que ça donne" !

Dans le cadre des exercices de cet article, si vous souhaitez vous imposer des règles, dans ce cas :

  • N'utilisez pas de récursion explicite (pas de def fact(n: Int): Int = ... n * fact(n - 1)). Mais utilisez des opérations qui peuvent l'utiliser (map, foldLeft, groupBy, flatMap...) ;
  • N'utilisez le if que si vous ne pouvez pas faire autrement.

Si vous voulez d'autres règles applicables dans le cadre de la programmation fonctionnelle, vous pouvez vous inspirer des règles de functional calisthenics proposées par Jorge Gueorguiev Garcia sur le blog de Codurance.

Exercices en Scala

Vous avez dans cette section la liste d’exercices que je donne dans les projets que je rencontre. Je ne fournis pas les solutions. Mais si vous souhaitez en discuter, vous pouvez me contacter sur mon compte Twitter : @fsarradin.

Pour tester votre code, normalement les Scala worksheet de IntelliJ IDEA ou d'Eclipse devraient suffir, ainsi que le site Scastie.

Une liste d'adresses

Compléter addressOf afin de fournir une liste d'adresses à partir d'une liste de User.

case class User(name: String, address: String)

def addressOf(users: List[User]): List[String] = ???

Somme de salaires

Compléter salarySum pour calculer une somme de salaire à partir d'une liste de Employee.

case Employee(name: String, salary: Double)

def salarySum(employees: List[Employee]): Double = ???

Addition d'Options

Compléter add de telle sorte que cette fonction calcule la somme des entiers contenus dans les deux options passées en paramètres (si elles contiennent un entier).

def add(o1: Option[Int], o2: Option[Int]): Option[Int] = ???

⇒ Réaliser l'implémentation en utilisant des if, en utilisant le pattern matching, en utilisant map/flatMap et en utilisant un for-comprehension (ie. for { ... } yield ...).
⇒ L'exercice vous laisse décider du meilleur comportement à mettre en place. N'hésitez pas à recommencer plusieurs fois l'implémentation et à la mettre en situation.

Moyenne

Compléter average de telle sorte que cette fonction calcule la moyenne des valeurs de l'iterator en une passe.

def average(values: Iterator[Double]): ??? = ???

⇒ Il est à noter que le type Iterator en Scala (et dans les autres langages) ne permet qu'une passe.
⇒ Est-ce que le type de sortie de average doit être Double uniquement ? Que se passe-t-il si l'iterator est vide ?

Maybe

Le type Maybe représente le fait d'avoir une valeur présente ou pas. Il est équivalent au type Option de Scala. Cependant, le code ci-dessous ne compile pas. Compléter cet extrait pour qu'il passe la compilation et que Maybe ait un comportement proche du Option pour les opérations map et flatMap.

sealed trait Maybe[+A] {
 def map[B](f: A => B): Maybe[B]
 def flatMap[B](f: A => Maybe[B]): Maybe[B]
}
case class Yep[A](value: A) extends Maybe[A]
case object Nope extends Maybe[Nothing]

⇒ Vérifier que Maybe fonctionne avec un for-comprehension :

println(for { a <- Yep(1); b <- Nope: Maybe[Int] } yield a + b) // display Nope

⇒ Bonus : dans Maybe directement, écrire map en fonction de flatMap.

Évaluation d'expression

Compléter la fonction eval qui permet d'évaluer une expression décrite avec l'ADT (Algebraic Data Type) ci-dessous.

sealed trait Expression
case class Const(value: Double) extends Expression
case class Add(left: Expression, right: Expression) extends Expression
case class Mult(left: Expression, right: Expression) extends Expression

def eval(exp: Expression): Double = ???

eval(Add(Const(1), Const(2))) == 3
eval(Mult(Const(2), Add(Const(1), Const(3)))) == 8
⇒ À noter qu'un ADT peut être vu comme un type composite dont les éléments sont analysables en utilisant le pattern matching.

JSON to String

Compléter la fonction jsonToString qui permet de convertir en String un document JSON décrit avec l'ADT (Abstract Data Type) ci-dessous.

sealed trait JsonValue
case object JsonNull extends JsonValue
case class JsonBoolean(value: Boolean) extends JsonValue
case class JsonNumber(value: Double) extends JsonValue
case class JsonString(value: String) extends JsonValue
case class JsonArray(value: List[JsonValue]) extends JsonValue
case class JsonObject(value: Map[JsonString, JsonValue]) extends JsonValue

def jsonToString(json: JsonValue): String = ???

jsonToString(JsonObject(Map(JsonString("a") -> JsonNumber(10.0)))) == """{"a":10.0}"""

Est-ce trié ?

Compléter la fonction isSorted qui vérifie qu'une liste est triée. La fonction ordered fournie en paramètre indique si deux éléments sont ordonnés.

def isSorted[A](l: List[A], ordered: (A, A) => Boolean): Boolean = ???

Top des mots

Compléter la fonction top10Words qui fournit depuis un texte les 10 mots les plus utilisés. On ne s'intéresse qu'aux mots de 10 lettres ou plus.

def top10Words(text: String): Map[String, Int] = ???

⇒ La fonction retourne une Map qui pour chaque mot associe son occurrence dans text.
⇒ Texte d'exemple : http://www.gutenberg.org/cache/epub/5781/pg5781.txt

Signatures

Trouver des fonctions ayant les signatures suivantes :

val f0: () => String = ???
val f1: String => String = ???
val f2: (String, String) => String = ???
val f3: String => String => String = ???
val f4: (String => String) => String = ???
val f5: String => (String => String) => String = ???

Conclusion

N'hésitez pas à partager vos réponses si vous le souhaitez. Mais n’hésitez pas non plus à proposer d’autres exercices.

D’une manière générale, les exercices (mais on peut parler plus généralement de mise en situation) sont un concept, qui à mon sens doit faire partie de la boîte à outils de tout bon coach/mentor. Un bon exercice doit être assez court pour être résolu dans la demi-heure et donner l'impression d'être accessible. Sa difficulté doit être adaptée au niveau de l'équipe. Il ne doit toutefois pas paraître ni évident ni trop dur, sinon vous risquez de perdre l'intérêt des développeurs. Il faut donner envie de le résoudre. Cela veut dire aussi qu'il est difficile de proposer des exercices dans lesquels il faut construire une application complète.

Autre point, si plusieurs nouveaux arrivants apparaissent dans l'équipe, vous pouvez proposer les exercices donnés auparavant. Cela a deux conséquences : 1/ les nouveaux membres se mettent à niveau, 2/ les membres déjà présents en refaisant les exercices perçoivent mieux l'évolution de leur niveau, ont la possibilité de se faire réexpliquer les concepts et peuvent même intervenir en tant que mentor pour les nouveaux.

Un bon exercice doit aussi permettre de forcer à réfléchir et de discuter sur les concepts mis en avant. C’est une invitation à l'expérimentation et au dialogue.