vendredi 22 février 2013

Why Scala: La stack (1/2)








Dans mon
billet précédent, j'expose mon retour d'expérience concernant Scala. Maintenant que je possède le langage (enfin quelques éléments), que me manque-t-il pour coder une application de la vraie vie? Un outil de build, une couche pour faciliter les accès JDBC et un framework web me paraissent être le minimum. Je vais me pencher sur les cas de
SBT,
Slick et
Play Framework 2.1 (what else?).


SBT




C'est l'outil de build qui me semble le plus répandu pour le développement Scala. Il est incontournable pour Play Framework (la console se base dessus). Si on regarde un peu en arrière,  pour nous délivrer de javac, Apache à créé Ant. C'était bien mais trop freestyle et des configurations complexes ont été engendrées en masse. Qu'à cela ne tienne, puisque la gestion technique du projet nécessite un outil plus coercitif, Apache nous apporte une fois de plus la lumière avec Maven (on va dire que l'ampoule a été vissée à partir de Maven 2). Aujourd'hui les critiques fusent autour du manque de flexibilité. Comme tous, j'ai du faire l'investissement autour de Maven il y a quelques années, il n'est pas parfait mais je le maîtrise et il fait le taf, au prix de quelques acrobaties parfois. 




Je trouve que l'outil de build est un non sujet. Que l'application soit construite avec Ant, Maven, Graddle ou des scripts shell, quelle différence pour l'utilisateur? Pire: pour le code? Devoir encore passer du temps sur SBT me paraît être une perte sèche et m'irrite un peu je l'avoue. Je me limite donc à des copier-coller de config et aux commandes compile, test et idea. Pas d'appréciation.



Slick





Anciennement ScalaQuery, Slick est un framework d'accès aux données en Scala. Il propose trois voies d'accès: 






  • plain SQL queries: à priori peu d'intérêt, je ne me suis peu attardé dessus

  • lifted embedding: j'ai présenté les possibilités de la boucle for en Scala dans mon précédent billet et notamment le sentiment que l'on peut interroger les collections comme des tables relationnelles. Et c'est ce que ce mode propose et c'est ce qui m'a d'ailleurs attiré vers Slick:



//Domain definition
case class Supplier(id:Int,name:String)

//DAO
object Suppliers extends Table[Supplier]("SUPPLIER"){
//Column definition
def id = column[Int]("id", O.PrimaryKey)
def name = column[String]("name")

//Projection definition
def * = id ~ name <>(Supplier, Supplier.unapply _)
}

//Interrogation
val q = for {
s < - Suppliers
if s.name === "my supp"
} yield s.id

q.list/* have fun here with Scala lists!*/.foreach(println)


Slick propose un troisième mode, direct embedding, l'exemple va vous rappeler quelqu'un:

@table(name="COFFEES")
case class Coffee(
@column(name="NAME")
name : String,
@column(name="PRICE")
price : Double
)


Et là comme j'ai pu le faire, vous êtes en train de vous dire que c'est du JPA! Enfin ça le sera peut être un jour car la doc stipule deux bloqueurs:


The direct embedding currently only supports database columns, which can be mapped to either
String, Int, Double.
Ouch même pas les dates!



The direct embedding currently does not feature insertion of data. WTF? On peut interroger des données qu'on a pas pu insérer? Pas exactement, il faut utiliser un des deux autres modes pour persister.





Mes attentes principales autour d'un outil d'accès aux données sont de générer le SQL pour me faire rêver que le changement de SGBD sera sans impact sur le code et des fonctions de mapping relationnel-objet pour limiter le code boilerplate. Du coup des trois modes il n'en reste qu'un: lifted embedding.




Le langage de requêtage basé sur la boucle for est fort séduisant:




  • il est très concis et s'intègre agréablement dans le code, après je ne l'ai pas challengé avec un schéma de 200 tables non plus...

  • il est statiquement typé: pour avoir la même chose avec JPA 2.0, il faut générer un méta modèle et utiliser l'API criteria (mon royaume pour une corde et un arbre que j'aille me pendre…)






Je mène ces explorations par pure convenance personnelle et ce sont les technologies traditionnelles qui me permettent de ramener le pain quotidien, il est donc facile de deviner que ma référence en la matière est JPA. D'ailleurs dans les différences fondamentales, on notera deux points marquants:




Les entités mappées ne sont pas managées: le framework n'a aucune idée de la situation d'un objet du domaine au regard de son état en base; de plus il n'est même pas obligatoire d'associer un DAO avec une classe, il peut tout aussi bien exploiter des tuples.




Les relations ne sont donc pas portées par le modèle métier mais par les DAO, ce qui implique que la navigation dans le graphe passe nécessairement par une interrogation explicite. Imaginons l'extension de l'exemple précédent:



      object Coffees extends Table[Coffee]("COFFEE"){
def id = column[Int]("id", O.PrimaryKey)
def name = column[String]("name")
def supplierId = column[Int]("sup_id", O.NotNull)

def * = id ~supplierId~name <>(Coffee,Coffee.unapply _)
def supplier = foreignKey("fk_supId", supplierId,Suppliers)(_.id)

}

(Suppliers.ddl ++ Coffees.ddl).create

Suppliers.insert(Supplier(1, "my supp"))

Coffees.insert(Coffee(1,1,"Ristretto"))

val q = for {
c < - Coffees
s < - c.supplier
} yield (s.name, c.name)
q.list.foreach(println)
}



De prime abord, ces points semblent être des lacunes comparés à JPA. Mais en prenant un peu de recul, lors de la plupart (hmm… peut être même la totalité?) de mes interventions sur les projets, j'ai pu observer que l'ORM était souvent hors de contrôle. Oui, "sans maîtrise la puissance n'est rien" (j'adore), et toute la magie apportée par l'instrumentation des classes, les invocations d'assesseurs interrogeant automatiquement la BDD, la fusion d'entités et j'en passe, éclipsent totalement la mécanique sous-jacente. Le résultat: des torrents de SQL noient littéralement nos bases, devant également faire face à des requêtes monstrueuses aux jointures improbables, impliquant des plans d'exécution moins accessibles que le Saint Graal lui même, et souvent pour afficher moins de colonnes que les doigts d'une main. Ne nous trompons pas, je ne dresse pas le procès de JPA, une très belle technologie, mais celui de ses utilisateurs.




Alors finalement, moins de fonctionnalités réduit la fracture entre les développeurs et la modélisation relationnelle. Slick semble parvenir à ce compromis: il concilie un niveau d'abstraction acceptable en gardant à sa charge les basses besognes, telles que la génération du SQL, avec un code qui laisse l'informaticien conscient des concepts mis en oeuvre. A méditer.









Chers internautes, je me dois maintenant d'interrompre ce séjour sur l'île aux enfants pour nous ramener dans la vraie vie, alors dites au revoir à Casimir et préparez vous à la descente.









D'abord les snippets présentés sont évidemment résumés dans un souci de lisibilité. Slick exploite le côté obscur de Scala (selon moi évidemment), les wrappers implicites. Pour que notre boucle for appliquées aux tables compile il faut importer un driver permettant d'intégrer le langage de Slick avec la source de données JDBC (à l'image du dialecte Hibernate). Ensuite, il faut ouvrir ou récupérer la session courante:



import Database.threadLocalSession
import scala.slick.driver.H2Driver.simple._

Database.forURL("jdbc:h2:mem:test1", driver = "org.h2.Driver") withSession {
val q = for(....
}




Admettons devoir intégrer ce code dans une application, l'URL et le driver JDBC, on peut les caser dans un fichier de conf, mais pour le driver Slick on fait quoi? On ne va pas laisser des H2Driver traîner dans toutes les classes, et puis c'est bon pour les tests ou les POC mais quand le temps viendra d'utiliser une vraie base on fera quoi? La solution se base sur le cake pattern qui modélise l'injection de dépendance statiquement typée en Scala (plus de détails sur
http://jonasboner.com/2008/10/06/real-world-scala-dependency-injection-di/). Je vous laisse consulter l'exemple fourni sur
https://github.com/slick/slick-examples/blob/master/src/main/scala/com/typesafe/slick/examples/lifted/MultiDBCakeExample.scala#L61. Il présente un gâteau avec trois ingrédients: l'encapsulation du driver Slick, le compsant d'accès aux données et les DAO. Je le trouve nettement plus indigeste que:


@PersistenceContext
private EntityManager em;



Affaire de goût? 




Les exemples fournis se basent uniquement sur la session, quid de la gestion transactionnelle? En parcourant la documentation de référence, je n'ai rien trouvé. Cependant j'ai réussi à dénicher qu'il fallait d'abord ouvrir une session puis regrouper toutes les commandes dans un bloc transactionnel:



Database.forURL("jdbc:h2:mem:test1", driver = "org.h2.Driver") withSession {
threadLocalSession.withTransaction{
[...]
}
}







Personne ne leur à montré @Transactionnal? Nous voilà quelques années en arrière…






Slick propose les query templates qui permettent d'exécuter une requête plusieurs fois avec des paramètres différents. Nous savons tous combien les optimiseurs SQL apprécient cette attention et nous la rendent avec des temps d'exécution diminués. Seulement les query templates ne couvrent que l'interrogation (select), ce qui implique que les manipulations (update/delete) paramétrées ne sont disponibles qu'au travers du mode plain SQL queries. Une lacune difficilement excusable, gageons qu'il s'agit d'un problème de jeunesse.









La documentation d'API n'a que le mérite d'exister, les développeurs doivent respecter une des lois XP qui précise que si des commentaires sont nécessaires c'est que le code n'est pas clair… Bref, une Scala Doc qui est tout bonnement anémique et qui daigne nous lâcher royalement une phrase de description dans quelques une des classes.









Je trouve Slick prometteur, car une fois que tous les petits défauts de jeunesse seront comblés, Scala bénéficiera d'une technologie d'accès aux données qui sera parfaitement intégrée au langage. En revanche passer de la version 0.11 à la version 1.0 (releasée il y a peu) est un peu rapide. 1.0 me paraît être un jalon dans lequel les fonctionnalités de base sont comblées, avec éventuellement quelques bogues ou une intégrabilité balbutiante après tout 1.0 est un gage de jeunesse. Seulement voilà, l'embedded mode est tout simplement inexploitable et le lifted mode incomplet. N'aurait-il pas été plus utile au projet de finaliser le deuxième avant de s'aventurer sur le premier? Mais ne jetons pas le bébé avec l'eau du bain, l'évolution de ce framework reste définitivement à surveiller, même si son numéro de version actuel devrait être 0.8 au lieu de 1.0. 








M'épancher sur le cas de Play ferait un billet assez long, donc je m'arrête là et vous proposerais la suite rapidement c'est promis!



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)...