samedi 2 février 2013

Why Scala?

Le geek est une espèce qui présente un comportement addictif au renouveau perpétuel.
Les geeks se découpent en plusieurs castes dont une d'entre elles regroupe ceux qui codent en Java. Depuis quelques temps les membres de celle-ci souffrent car ce langage ne bouge guère. Pour répondre à ce mal, fleurissent des langages alternatifs qui s'exécutent tout de même sur la JVM, dont un en particulier: Scala. Il entraîne d'ailleurs la naissance de guerres de clocher au sein même de la caste. 
Tiraillé par le manque d'évolution (quoi je suis le seul qui n'a même pas eu une demi molle en découvrant le diamond operator de Java7??) et curieux de comprendre ce qui défraie autant de passion, j'ai décidé de monter en compétence sur cette technologie (pas si nouvelle que ça d'ailleurs - 2003). J'ai mangé le pavé de Martin Odersky 'Programming in Scala', transpiré sur les katas S-99, suivi la session proposé par Coursera et quelques mois plus tard, j'aimerais dresser mon bilan de l'expérience.

Ce qui m'a plu


Scala est un langage permettant de résoudre les problèmes par l'approche fonctionnelle (mais pas exclusivement car il reste compatible avec l'orienté objet) et il m'a donc été nécessaire d'appréhender les concepts liés à ce style de programmation. Voici un florilège de ce que j'en ai apprécié:
Evidemment les lambdas. Un buzzword qui fait beaucoup couler d'octets sur la toile, un des fondements de la programmation fonctionnelle, le saint graal qui doit permettre de tirer aisément parti des architectures multicores. J'avoue être un poil perdu: les lambdas sont-elles un concept fondamental ou simplement du sucre syntaxique. Une chose est claire: le code en est plus concis et je kiffe.
 Un des principaux intérêts des lambdas est sont intégration dans le SDK, donc ce n'est pas un scoop, les listes en Scala, ça poutre.

La boucle for est sacrément revisitée: fini les boucles imbriquées grâce à cette nouvelle syntaxe qui permet d'intégrer des produits cartésiens ainsi que filtres de façon élégante… on a l'impression de requêter les collections comme on le ferait avec des tables relationnelles. Quelques exemples sympas:
Produit cartésien:
scala> for ( i <- 1 to 2; j <- List("un","deux")) yield(i,j)
res4: scala.collection.immutable.IndexedSeq[(Int, java.lang.String)] = Vector((1,un), (1,deux), (2,un), (2,deux))

Jointure
scala> for ( i <- 1 to 2; j <- List((1,"un"),(2,"deux")) ; if i==j._1) yield(i,j)
res5: scala.collection.immutable.IndexedSeq[(Int, (Int, java.lang.String))] = Vector((1,(1,un)), (2,(2,deux)))


L'inférence de type. Il trouve tout seul:
scala> val i=0
i: Int = 0
Le compilateur n'est plus malentendant et il n'est plus nécessaire de lui répéter plusieurs fois le type. Scala mise définitivement sur la concision du langage.

Les fonctions partielles ou Currying. Imaginez une fonction pour laquelle seule une partie des paramètres est définie, mais ça donne quoi? Une fonction qui attends le reste de paramètres. Cool non? Exemple:
scala> def stupidAdditionExample(i:Int)(j:Int)=i+j
stupidAdditionExample: (i: Int)(j: Int)Int

scala> val partial = stupidAdditionExample(2)_
partial: Int => Int = 

scala> partial(3)
res0: Int = 5


L'immutabilité. Bloch en vante largement les vertus pour éviter d'avoir à poser des verrous pour synchroniser les accès concurrents dans Effective Java , mais franchement dans un quotidien de développement d'application web de gestion reposant sur des conteneurs et JPA, on est content de le savoir mais on a l'impression de n'être que partiellement concerné. Dans Scala on ne peut pas passer à côté et les variables ne deviennent qu'une possibilité optionnelle. En plus des considérations techniques avancées par Bloch, la programmation fonctionnelle ajoute le fait que la mutabilité des variables n'est issu d'aucun concept mathématique ou algorithmique mais est simplement le reflet de la possibilité de modifier le contenu des registres du socle matériel. La mutabilité n'est pas la réalité et c'est mal pour la concurrence. J'ai cru au début que ça allait piquer car il faut lutter contre de vieilles habitudes et en fait non… je me suis vu contraint de faire appel à une variable une seule fois (une sale histoire d'InputStream, je ne peux rien dire de plus la cicatrice est encore fraîche).  

"Je te donne peut-être une valeur… et peut-être pas", ça ce sont les options. L'intérêt n'est pas flagrant de prime abord mais du coup tous les "if null else" disparaissent (et les NPE de surcroît) . De plus l'API offre la possibilité de modifier le contenu et de définir des valeurs par défaut contextuelles:
scala> val peutetre=Some(1)
peutetre: Some[Int] = Some(1)

scala> peutetre.get
res1: Int = 1

scala> val rien=None
rien: None.type = None

scala> rien.get
java.util.NoSuchElementException: None.get

scala> rien.getOrElse(1)
res3: Int = 1

scala> peutetre.map(i=> "valeur = " + i).getOrElse("Rien")
res6: java.lang.String = valeur = 1

scala> rien.map(i=> "valeur = " + i).getOrElse("Rien")
res7: java.lang.String = Rien


Les applications nécessitent souvent un singleton et les patterns compliqués et souvent buggés ont longtemps fleuri, la réponse de Spring a été de fournir des singletons de fait et enfin Java 5 a permis de mettre tout le monde d'accord grâce à une utilisation dérivée des Enum. Mais voilà si cette dernière option est techniquement justifiée, elle reste sémantiquement très discutable. Qu'à cela ne tienne, puisqu'il s'agit d'un besoin récurrent Scala l'intègre dans le langage avec l'object. Efficace et pertinent.

L'un des leitmotiv de Scala est que la réduction du nombre de lignes de code d'une application à périmètre fonctionnel constant réduit les probabilités de bugs. Donc pour permettre la diminution du code "boilerplate", le langage apporte une quantité importante de sucre syntaxique. Le geek est friand par nature du sucre syntaxique. J'ai kiffé. Quelques exemples:

Qui n'a jamais rêvé de la possibilité de définir la structure d'un bean anémique avec un oneliner? Les case class le permettent car seuls les membres sont définis, le compilateur s'occupe des assesseurs, du constructeur et des méthodes equals/hashcode (et plus encore).
scala> MyBean(2).equals(3)
res8: Boolean = false

scala> MyBean(2).equals(MyBean(2))
res9: Boolean = true


Les placeholders paraissent de prime abord un peu rugueux mais ils deviennent vite habituels. En gros, ils permettent de ne pas déclarer ni nommer les paramètres qu'une closure utilise en fonction de leur position. Ainsi la réduction suivante
scala> List(1,2,3).foldLeft(0)((acc,elem)=>acc+elem)
res19: Int = 6
Peut s'écrire également:
scala> List(1,2,3).foldLeft(0)(_+_)
res18: Int = 6
Il y a plein d'autres utilisations possibles des placholder (ils ont été utilisés pour présenter les fonctions partielles notamment).

Un des atouts des case class est leur utilisation avec le pattern matching. Il s'agit d'une autre forme d'instanceOf basé  sur les extracteurs (tiens encore une fonctionnalité intégrée aux case classes). Je vous livre en l'état un exemple très incorrect mais qui permet une illustration succincte du concept:
scala> MyBean(3) match { case MyBean(i) => i+2 }
res20: Int = 5
Cela fonctionne que parce que la définition d'une case class implique la création d'un objet compagnon qui fourni un extracteur:
scala> MyBean.unapply(MyBean(3))
res4: Option[Int] = Some(3)


La liste pourrait s'allonger de façon assez ennuyeuse: pas besoin de ';', pas besoin de parenthèses pour les fonctions sans paramètres, contrôle sur l'évaluation des paramètres (by name / by value), etc. Mais je vais m'en tenir là et passer aux notes un peu plus douloureuses.

Ce qui m'a déplu


La conversion implicite, au moyen de wrappers, est une fonctionnalité qui permet de décorer automatiquement les objets et par ce biais de leur ajouter des méthodes. Cela apporte la liberté syntaxique d'un langage dynamique dans un langage basé sur le typage. Séduisant non? Exemple:
scala> class StringWrapper(s:String){
     | def taille=s.length
     | }
defined class StringWrapper

scala> implicit def toWrapString(s:String)=new StringWrapper(s)
toWrapString: (s: String)StringWrapper

scala> "yes".taille
res0: Int = 3
Mais pour les activer il faut souvent les importer, de plus naviguer dans les API d'une bibliothèque tierce devient un enfer. J'aime pas.

La programmation fonctionnelle mise sur des conceptions basées sur des appels récursifs. Seulement voilà, la JVM est la plateforme d'exécution et elle n'apprécie qu'avec modération cette pratique (gare à la StackOverflowError). Pour contourner cette limitation, Scala suggère d'utiliser la 'tail recursion' qui se caractérise par un appel récursif en dernière instruction de la fonction. Cette pratique lui permet de modifier le code à la compilation en boucle for. Le bémol est que l'abstraction du language vis à vis des considérations techniques est brisée car la conception est stigmatisée par les limites de la JVM. C'est pas sa faute mais j'aime pas.

Variance, covariance, contravariance, nonvariance… les possibilités de contrôle des paramètres de type sont complètes… et donc également complexes… Difficile de s'y retrouver sans avoir la doc ouverte au bon chapitre. Un exemple:
trait MapLike[A, +B, +This <: MapLike[A, B, This] with Map[A, B]] extends collection.MapLike[A, B, This] withParallelizable[(A, B), ParMap[A, B]]
Le code est issu du SDK (ici) Je me m'aventurerais pas à en nier la pertinence, en revanche je suppute que l'être vivant qui déchiffre ces paramètres d'une traite est sûrement capable de donner le nombre exact d'allumettes qui viennent de tomber par terre...

Lorsque l'on tente de dériver une case class dans une autre case class, voici le message produit par le compilateur (avec l'option deprecation):
scala> case class MyBean(id:Int)
defined class MyBean

scala> case class MySubBean(name:String) extends MyBean(2)
:9: warning: case class `class MySubBean' has case ancestor `class MyBean'.  
Case-to-case inheritance has potentially dangerous bugs which are unlikely to be fixed.  
You are strongly encouraged to instead use extractors to pattern match on non-leaf nodes.

Y a sûrement une excellente raison, mais le message est déroutant.

Les paramètres implicites font à mes yeux partie de ces concepts qui apportent de la magie à un langage... enfin je dirais plutôt de la sorcellerie:
scala> def imprime(implicit x:Int)=println(x)
imprime: (implicit x: Int)Unit

scala> implicit val test=3
test: Int = 3

scala> imprime
3
Simple dans cet exemple, mais vous vous doutez bien que les règles liées ne le sont pas autant et si une autre valeur implicite traîne dans les parages, les choses se corsent. Bref je trouve cette fonctionnalité dangereuse.

Un détail ennuyeux: le code Scala compilé l'est pour une version précise de scala ce qui a notamment entraîné l'apparition d'une nouvelle dimension dans les coordonnées des dépendances...

"With great power comes great responsibility..." Scala m'apparaît conçu pour apporter une solution à tous les problèmes qui ont pu être levés dans  les langages auparavant et tente d'intégrer toute fonctionnalité séduisante. Cela le rend il universel? S'il l'est, il l'est pour des développeurs universels! Je le trouve assez élitiste, ce qui me semble être son principal défaut et un frein à son adoption. 

Ma conclusion



S'il est difficile d'attendre d'un langage d'être l'élément qui garanti la réussite d'un projet, Scala prend à sa charge des sujets sensibles tels que la concurrence, les threads et j'en passe. C'est un plus car le développeur peut augmenter son attention sur le code métier. 
C'est un langage qui mérite sa place dans l'ecosystème, pas comme un leader du secteur, mais peut-être comme un Concept Car ou une Formule 1, la plebe dont nous faisons partie espérant voir quelques unes de ses avancées alléchantes déclinées dans  nos outils quotidiens... Et puis le langage est une chose mais qu'est il sans une stack de développement? C'est un autre sujet (to be continued)...

1 commentaire:

Loïc Descotte a dit…

Un bon lien sur les implicits (voir surtout le point 4.) https://www.bionicspirit.com/blog/2012/11/02/scala-functional-programming-type-classes.html

Le cas que tu montres avec le int implicite fait un peu peur mais ça a une vrai utilité, et ça apporte pas mal de flexibilité aux API