É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]
    • None ~ Some(Nil)
  • List[Option[T]] -> List[T]
    • maListDoption.flatten

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

Questions ?