Un joueur est ajouté à notre application.
{ "name": "Batman", "level": 99, "hp": 85 }
Le JSON est parsé et les erreurs de structure gérées.
case class Player(name: String,
level: Int,
hp: Int)
On veut valider ces données avec des critères métier.
// On veut implémenter cette fonction
def validate(player: Player): ValidPlayer = ???
case class ValidPlayer(name: String,
level: Int,
hp: Int)
// ^ Même structure que Player
case class Player(name: String,
level: Int,
hp: Int)
Pour qu'un joueur soit valide (et devienne un ValidPlayer
), il doit respecter ces 3 critères :
95 + niveau * 5
Longueur de nom supérieur à 3 caractères :
def validateName(name: String): Boolean =
name.size >= 3
Niveau strictement positif :
def validateLevel(level: Int): Boolean = level > 0
Moins de points de vie que 95 + niveau * 5
:
def validateHp(level: Int, hp: Int): Boolean =
hp <= 95 + level * 5
validate
peut échouer à produire une valeur de type ValidPlayer
.
def validate(player: Player): ValidPlayer = ???
Comment le gérer ?
Deux points de vue :
- définition de
validate
- appel de
validate
Si validate
échoue, on renvoie null
.
def validate(player: Player): ValidPlayer = {
if ( /* ... */ ) {
ValidPlayer( /* ... */ )
}
else null
}
val player = Player( /* ... */ )
val validPlayer = validate(player)
if (validPlayer != null) {
// On peut utiliser validPlayer
}
Que ce passe-t-il si on oublie le test et que la validation a échoué ?
if (validPlayer != null) {
/* ... */
}
java.lang.NullPointerException
null
null
est une valeur valide pour tous les typesDe manière générale :
Ne jamais utiliser
null
.
def validate(player: Player): ValidPlayer = {
if (!validateName(player.name)) {
throw new RuntimeException("Invalid name")
}
if (!validateLevel(player.level)) {
throw new RuntimeException("Invalid level")
}
if (!validateHp(player.level, player.hp)) {
throw new RuntimeException("Invalid HP")
}
ValidPlayer(player.name, player.level, player.hp)
}
try {
validate(p)
}
catch {
case e: RuntimeException => /* Réparer ou propager l'erreur */
}
Pas de checked exceptions
en Scala.
Pokemon Driven Development: Gotta catch 'em all!
throw
stoppe l'exécution de la fonctiontry
/catch
, l'exception se propagecatch
>
/dev/null
#vuDansLaVraieViethrow
est de type Nothing
Pas très adapté pour gérer des erreurs métier prévisibles...
Une erreur métier est un résultat comme un autre.
Comment peut-on représenter ces erreurs métier en Scala ?
case object NameTooShort
case object InvalidLevel
case class TooManyHp(current: Int, max: Int)
Algébrique ?
MyType = Int + String
MyType = Int x String
abstract sealed trait VE
// ^^ Pour ValidationError
// ^^^ Très important !
case object NameTooShort extends VE
case object InvalidLevel extends VE
case class TooManyHp(current: Int, max: Int) extends VE
Uniquement 3 manières de créer une valeur de type VE
:
val e1: VE = NameTooShort
val e2: VE = InvalidLevel
val e3: VE = TooManyHp(current, max)
scalacOptions ++= Seq(
"-unchecked",
"-deprecation",
"-feature",
"-Xfuture",
"-Xlint",
"-Xfatal-warnings"
)
None
Some(x: A)
def validate(player: Player): Option[ValidPlayer] = {
if ( /* ... */ ) {
Some(ValidPlayer( /* ... */ ))
}
else None
}
Suffisant si on n'a pas besoin d'information sur l'erreur.
val player = Player( /* ... */ )
val validPlayer = validate(player) // Option[ValidPlayer]
// Modifier sans traiter l'erreur :
val playerName = validPlayer.map(p => p.name)
// ^ Option[String]
// Accéder à la valeur :
validPlayer match {
case None => // On gère l'erreur
case Some(p) => // On peut utiliser p
}
// Fournir une valeur par défaut :
playerName.getOrElse(Player( /* ... */ ))
import scala.util.Either
Left(x: A)
Right(x: B)
def validate(player: Player): Either[VE, ValidPlayer] = {
if (!validateName(player.name)) {
Left(NameTooShort)
}
else if (!validateLevel(player.level)) {
Left(InvalidLevel)
}
else if (!validateHp(player.level, player.hp)) {
Left(TooManyHp(player.hp, 95 + player.level * 5))
}
else {
Right(ValidPlayer(player.name, player.level, player.hp))
}
}
val player = Player( /* ... */ )
val validPlayer = validate(player) // Either[VE, ValidPlayer]
// Modifier sans traiter l'erreur :
val playerName = validPlayer.right.map(p => p.name)
// ^ Either[VE, String]
// ^^^^^^ Pas dingue :/
validPlayer match {
case Right(p) => // On peut utiliser p
case Left(NameTooShort) => // On gère l'erreur
case Left(InvalidLevel) => // On gère l'erreur
case Left(TooManyHp(current, max)) => // On gère l'erreur
// Warning du compilateur si on oublie un cas \o/
}
Failure(exception: Throwable)
Success(value: T)
Similaire à Either
:
Try[T]
~= Either[Throwable, T]
Throwable
)An extension to the core Scala library for functional programming.
libraryDependencies +=
"org.scalaz" %% "scalaz-core" % "7.2.7"
\/
(disjunction)NonEmptyList
Validation
import scalaz.{ \/, -\/, \/- }
-\/(x: A)
\/-(x: B)
def validate(player: Player): VE \/ ValidPlayer = {
if (!validateName(player.name)) {
-\/(NameTooShort)
}
else if (!validateLevel(player.level)) {
-\/(InvalidLevel)
}
else if (!validateHp(player.level, player.hp)) {
-\/(TooManyHp(player.hp, 95 + player.level * 5))
}
else {
\/-(ValidPlayer(player.name, player.level, player.hp))
}
}
Similaire à Either
, mais part du principe que la valeur intéressante est à droite (right-biased).
eitherVal.left.map(/* ... */) eitherVal.right.map(/* ... */)
disjunctionVal.leftMap(/* ... */) disjunctionVal.map(/* ... */)
A singly-linked list that is guaranteed to be non-empty.
List(xs: A*)
List() // Compile
List(1, 2, 3) // Compile
scalaz.NonEmptyList(h: A, t: A*)
scalaz.NonEmptyList() // Erreur
scalaz.NonEmptyList(1, 2, 3) // Compile
import scalaz.{ NonEmptyList, ValidationNel, Success, Failure }
import scalaz.syntax.applicative._
import scalaz.syntax.validation._
def validate(p: Player): ValidationNel[VE, ValidPlayer] = {
val vName = if (validateName(p.name)) Success(p.name)
else Failure[NonEmptyList[VE]](NonEmptyList(NameTooShort))
val vLevel = if (validateLevel(p.level)) Success(p.level)
else Failure[NonEmptyList[VE]](NonEmptyList(InvalidLevel))
val vHp = if (validateHp(p.level, p.hp)) Success(p.hp)
else {
val e = TooManyHp(p.hp, 95 + p.level * 5)
Failure[NonEmptyList[VE]](NonEmptyList(e))
}
/* ... */
}
def validate(p: Player): ValidationNel[VE, ValidPlayer] = {
val vName = /* ... */
val vLevel = /* ... */
val vHp = /* ... */
(vName |@| vLevel |@| vHp) { (n, l, h) =>
ValidPlayer(n, l, h)
}
}
Permet d'accumuler les erreurs lorsqu'on fait des validations indépendantes.
Rapture is a family of Scala libraries providing beautiful idiomatic and typesafe Scala APIs for common programming tasks, like working with I/O, cryptography and JSON & XML processing.
libraryDependencies +=
"com.propensive" %% "rapture-core" % "2.0.0-M7"
import rapture.core._
On wrap notre fonction validate
qui renvoie un Either
.
def validate(player: Player)(implicit mode: Mode[_]):
mode.Wrap[ValidPlayer, VE] = {
mode.wrapEither(validateEither(player))
// ^^^^^^^^^^^^^^
// def validateEither(p: Player): Either[VE, ValidPlayer]
}
On importe un mode à l'endroit de l'appel.
Par exemple, returnOption
:
def validateOption(player: Player): Option[ValidPlayer] = {
import modes.returnOption._
validate(player)
}
Ou returnTry
:
def validateTry(player: Player): Try[ValidPlayer] = {
import modes.returnTry._
validate(player)
}
Quelques modes actuellement disponibles :
modes.throwExceptions._ // default
modes.returnEither._ //missing?
modes.returnOption._
modes.returnTry._
modes.returnFuture._
modes.timeExecution._
modes.keepCalmAndCarryOn._
modes.explicit._
Orignal, mais pas prêt pour la production.
Pour gérer les erreurs métier :
Utiliser correctement ces types pour gérer les erreurs permet :