Écrire un scala idiomatique
Qui suis je ?
- Benoit Lemoine
- Développeur fullstack chez Captain Dash
- @benoit_lemoine
Idiomes
Une expression idiomatique est une expression particulière à une langue et qui n'a pas nécessairement d'équivalent littéral dans d'autres langues [...] (wikipédia)
En informatique, c'est un morceau de code (un pattern) reconnu par les utilisateurs d'un langage comme étant la façon la plus commune de répondre à un problème (moi)
Scala : Fonctionnel vs Objet
Objet
class
héritage
var
effet de bord...
Fonctionnel
case class
ADT
val
monades...
null
c'est nul
I call it my billion-dollar mistake. It was the invention of the null reference in 1965
Sir Charles Antony Richard Hoare
inventeur de quicksort... et de null
Fonction renvoyant null
def maMethod(param:String): String = {
if(param.length == 0) {
null
} else {
"{" + param + "}"
}
}
def maMethod(param:String): Option[String] = {
if(param.length == 0) {
None
} else {
Some("{" + param + "}")
}
}
Client pouvant consommer null
val str:String = monService.methodePouvantRenvoyerNull()
val len = if(str != null) str.length else 0
val str:Option[String] = Option(monService.methodePouvantRenvoyerNull)
val len = str.map(_.length).getOrElse(0)
Bannir .get
val myOption:Option[String] = None
myOption.get //NoSuchElementException
myOption.getOrElse("da")
myOption.orElse( Some("test"))
Éviter le pattern matching inutile
myOption match {
case Some(value) => Some(value.length)
case None => None
}
myOption.map(_.length)
myOption match {
case Some(value) => Some(value)
case None => Some("une nouvelle valeur")
}
myOption.orElse(Some("une nouvelle valeur"))
Eviter les options inutiles
- Option[List[T]] ~ List[T]
- List[Option[T]] -> List[T]
Exemple pratique
//Very Very Bad
case class User(id:Long, name:String)
val u = User.save(User(null, "new User"))
//Good
case class User(id:Option[Long], name:String)
val u = User.save(User(None, "new User"))
//Better
case class User(id:Long, name:String)
case class TemporaryUser(name:String)
val u = User.save(TemporaryUser("new User"))
//Best
case class UserId(id:Long) extends AnyVal
case class User(id:UserId, name:String)
Try
def strToInt(str:String): Option[Int] = try {
Some(str.toInt)
} catch {
case NumberFormatException => None
}
def strToInt(str:String):Option[Int] = Try(str.toInt).toOption
Try
Try(uneFonctionPouvantRenvoyerDesExceptions()) match {
case Success(v) => "my value" + v
case Failure(e:IllegalArgumentException) => "IllegalArgument"
case Failure(NonFatal(e)) => e.getMessage
}
Either
def strToInt(str:String): Either[String, Int] = Try(str.toInt) match {
case Success(i) => Right(i)
case Failure(e) => Left(e.getMessage)
}
val maybeN1: Either[String, Int] = strToInt("a")
val maybeN2: Either[String, Int] = strToInt("b")
val sum: Either[String, Int] = for(
n1 <- maybeN1.right;
n2 <- maybeN2.right
) yield (n1 + n2)
Try / Either / Option
- Try :
- Si un traitement peut échouer avec une exception
- Essayer de ne pas le renvoyer
- Either :
- Right est la valeur valide, Left l'erreur
- Si un traitement peut échouer avec un message ou un objet d'erreur
- Try[T] ~ Either[Throwable, T]
- Option :
- Si un traitement peut renvoyer une valeur ou non
- Ne pas utiliser si avec un
NullObject
Les Futures
Ne pas bloquer !
def monActionDeController(id:Long) = Action {
val maybeEventualUser:Future[Option[User]] = User.byId(id)
val maybeUser = Await.result(maybeEventualUser, 5 seconds)
maybeUser match {
case Some(user) => Ok(user)
case None => NotFound(s"user $id not found")
}
}
def monActionDeController(id:Long) = Action.async {
val maybeEventualUser:Future[Option[User]] = User.byId(id)
maybeEventualUser.map {
case Some(user) => Ok(user)
case None => NotFound(s"user $id not found")
}
}
Tests - ScalaTest
//trait Eventually - quand on a pas la main sur la Future
val xs = 1 to 125
val it = xs.iterator
eventually { it.next should be (3) }
//trait ScalaFutures - quand on reçoit la Future
whenReady(result) { s =>
s should be ("hello")
}
Immutabilité
var sum = 0
var count = 0
val words = List("une","liste","de","mots")
for(w <- words) {
sum += w.length
count += 1
}
val mean = sum / count
val sum = words.map(_.length).sum
val count = words.length
val mean = sum / count
For
comprehension
val maybeId:Option[Long] = Some(1)
val maybeUser:Option[User] = maybeId.flatMap(User.byId)
val maybeClassified:Option[Classified] = maybeUser
.filter(_.isCustomer)
.flatMap(Classified.byUser)
val maybeClassified:Option[Classified] = for(
id <- maybeId;
user <- User.byId(id);
classified <- Classified.byUser(user) if user.isCustomer
) yield classified
Pas de return
def contains(el:Long, l:List[Long]): Boolean = {
l.foreach { listElement =>
if(listElement == el)
return true
}
return false
}
// Pour la démo, fold
def contains(el:Long, l:List[Long]): Boolean = l.foldLeft(false) {
(result, listElement) => result || (listElement == el)
}
// Pour la démo, récursion
def contains(el:Long, l:List[Long]): Boolean = l match {
case Nil => false
case listElement :: t = (listElement == el) || contains(el, t)
}
// Dans le vrai monde
def contains(el:Long, l:List[Long]): Boolean = l.contains(el)
Tuple
val tuple = (2, 4, 5)
val somme = tuple._1 + tuple._2 + tuple._3
val tuple = (2, 4, 5)
val somme = tuple match {
case (a, b, c) => a + b + c
}
Case class
class Parent {
case class Animal(name:String)
def isDonald(a:Animal):Boolean = Animal("donald") == a
}
val aDonald = p.Animal("donald")
p.isDonald(aDonald) // true
val anotherDonald = new Parent().Animal("donald")
p.isDonald(anotherDonald) // ne compile pas
Algebraic Data Type - enum
sealed trait Planet
object Planet {
case object Mercury extends Planet
case object Venus extends Planet
case object Jupiter extends Planet
case object Saturne extends Planet
}
import Planet._
def isGiant(p:Planet):Boolean = p match {
case (Mercury | Venus) => false
case (Jupiter | Saturne) => true
}
Algebraic Data Type
sealed trait Permission
object Permission {
case object AccessDenied extends Permission
case class AccessPermited(id:Long) extends Permission
}
import Permission._
def hasAccess(p:Permission, searchedId:Long): Boolean = p match {
case AccessDenied => false
case AccessPermited(id) => id == searchedId
}
//Attention à
def hasAccess(p:Permission, searchedId:Long): Boolean = p match {
case AccessDenied => false
case a:AccessPermited => a.id == searchedId
}
Pattern matching
val myBool:Boolean = true
val r:String = myBool match {
case true => "Vrai"
case false => "Faux"
}
val r:String = if(myBool) "Vrai" else "Faux"
Injection de dépendance
trait MyService {
def myMethod():String = "call to my method"
}
object MyService extends MyService
trait MyController {
this: Controller =>
def myService:MyService
def myAction = Action { Ok(myService.myMethod())}
}
object MyController extends Controller with MyController {
lazy val myService = MyService
}
Injection de dépendance
//Dans les tests
class ControllerUnderTest(val myService:MyService)
extends Controller with MyController
val controller = new ControllerUnderTest(mockService)
Pour aller plus loin... Scalaz
- Either right-biased :
\/
- Either avec accumulation des Left :
Validation
- Option sans
.get
: Maybe
- List ne pouvant être vide : NonEmptyList
- etc.
Conclusion
Les signatures doivent expliquer ce que fait votre fonction
- Un traitement asynchrone : Future[_].
- Un traitement pouvant comporter une erreur : Try[_], Either[_,_]
- Un traitement pouvant ou non renvoyer un résultat : Option[_]
Conclusion
- Il faut connaitre les API de collection au dela de
foreach
, map
et filter
- Si c'est illisible ; c'est probablement faux