Skip to content

Instantly share code, notes, and snippets.

@addinquy
Last active October 26, 2016 08:55
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save addinquy/4358366 to your computer and use it in GitHub Desktop.
Débuter avec Ceylon

#Les mains dans le Ceylon ! ##(not yet) ready Ce mardi 18 décembre, nous étions invités par les duchesses à nous initier au langage Ceylon développé par Red Hat. Encore un nouveau langage sur la JVM ! Encore un "better java" ! Mais certaines idées ont attiré mon attention. Je ne voulais pas manquer cette occasion de m'y initier, en le faisant mieux et plus rapidement qu'en m'arranchant les cheveux tout seul devant ma machine. Bref, Emmanuel Bernard et Stéphane Epardaud ont joué les formateurs chez Capitain Train qui était notre hôte pour l'occasion (on les remercient chaleureusement).

ceylon2012-debut

[[MORE]]

De mon côté j'avais déjà passé un petit moment dimanche pour faire le setup de mon nouveau Mac Book. Pour bien commencer, je suis arrivé en retard et je me suis retrouvé au fond avec un peu de mal à lire le code projeté par notre duo de choc.
ceylon2012-separdaud
Place au code ! ##Hello Ceylon Commencer est très simple, surtout si on le fait via Eclipse avec le plugin Ceylon (ça faisait partie du setup pour la soirée). On crée simplement un projet Ceylon et un fichier source Ceylon. 3 choses à savoir : * Pas besoin de s'embêter avec une quelconque notion de package (ou de module dans le cas de Ceylon) si l'on en a pas besoin. * Pas de corrélation entre le nom de fichier et l'unité d'implémentation. On est donc libre de nommer notre fichier source .ceylon comme on veut. On peut même y faire figurer plusieurs unités d'implémentation. Comme en C++, quoi ! * Je parle d'unité d'implémentation : ce peuvent être des classes, des fonctions ou des variables ! Quand je vous parles du C++ ...

Donc nous créons notre fichier source run.ceylon et implémentons le très classique "hello world".

void helloWordl (){
  print("hello world");
}

Ce qui est un peu déroutant, c'est que l'on n'a rien déclaré en en-tête du fichier: on a donc droit à "print" gratos ! Améliorons un peu le truc pour découvrir d'autres aspects syntaxiques du langage:

void helloWordl (){
 value s = "hello world";
	String[] parts = {for (c in s.characters) if (c.lowercase) c.string};
	print(" ".join(parts...));
}

Il y a quatre choses à remarquer ici:

  • On peut initialiser une variable avec une expression qui peut contenir à peu près ce que l'on veut comme code Ceylon ! Mais attention, on ne peut y faire figurer une expression "multi-statements". En clair, pas de point-virgule dans une expression pour construire une séquence d'instructions ! Toutefois la limitation est contournable, car on peut appeler une fonction dans une expression.
  • Ceylon fait de l'inférence de type (du moins de l'inférence statique). Pas besoin d'en déclarer le type s'il peut être déduit. Suivant le cas de figure, il faudra faire précéder la variable du mot-clé evalue" ... ou non ! Bon, c'est certainement une question d'habitude...
  • Ceylon fait de l'autoboxing.
  • L'opérateur "..." permet de déterminer que l'on traite les String dans parts et non le tableau de String ! Là aussi il s'habituer !

En fait, on n'a pas besoin d'utiliser une expression avec une boucle pour ce traitement. On peut utiliser les "compréhensions" pour cela. Exemple.

void helloWordl (){
 value s = "hello World";
	print(" ".join(s.characters.filter((Character c) c.lowercase).map((Character c) c.string)...));
}

Ici, la partie intéressante permettant de filtrer les caractères et d'en récupérer le résultat est l'argument du join:

s.characters.filter((Character c) c.lowercase).map((Character c) c.string)

Dans tout ça, nous n'avons toujours pas importé quoi que ce soit, tout comme print, les types élémentaires et leurs méthodes sont à disposition sans qu'il soit nécessaire de déclarer quoi que ce soit. ##Ma première classe Ceylon Assez ri. Voyons maintenant à quoi ressemble l'écriture d'un vrai code sous forme de classes. Nous allons le faire sous la forme d'un petit pattern composite. Tout d'abord la classe de base abstraite.

abstract class Tree(){
 shared formal Integer evaluate();
}

Il y a déjà 3 choses à remarquer ici:

  • La déclaration de classe ressemble à une fonction ! En fait la déclaration de classe est un constructeur. Il peut prendre des paramètres en entrée qui sont alors disponibles pour le reste de la classe sans qu'il soit nécessaire d'en faire l'affectation sur des attributs !
  • La déclaration "shared" remplace "public".
  • La déclaration d'une méthode abstraite se fait par le mot-clé "formal".

Je viens de parler de mot-clé mais en fait, d'un point de vue technique, "abstract", "shared" et "formal" sont des annotations, au même titre que les annotations Java, sauf qu'il n'y a pas de "@". Déclarons maintenant une classe opérateur binaire abstraite.

class BinaryOperator(Tree left, Tree right, Integer f(Integer a, Integer b)) extends Tree() {
	shared actual Integer evaluate() {
		return f(left.evaluate(), right.evaluate());
	}
}

Ici nous remarquons deux nouveaux éléments.

  • D'abord une classe (ou une fonction, ou n'importe quoi qui prend des arguments) peut prendre une fonction en paramètre.
  • L'implémentation de notre méthode abstraite est déclarée "actual". C'est une annotation, comme on peut s'en douter.

On peut faire plus fort et plus drôle en introduisant une "killer feature" de Ceylon. Si on imagine que que notre composite va évaluer des entiers, notre opérateur binaires peut prendre des trucs compliqués à droite et à gauche. Mais il peut aussi prendre un littéral. Dans une langage OO classique, il nous faut wrapper ce littéral dans une classe polymorphe de Tree. Un artifice qui est surtout une contrainte de l'approche objet. Ceylon nous permet d'utiliser des unions de types. Exemple.

class BinaryOperator(Tree|Integer left,
			Tree|Integer right,
			Integer f(Integer a, Integer b)) extends Tree() {
	
	Integer resolve(Tree|Integer x) {
		switch (x)
			case (is Tree) {
				return x.evaluate();
			}
			case (is Integer) {
				return x;
			}
		}

	shared actual Integer evaluate() {
		return f(resolve(left), resolve(right));
	}
}

On remarque principalement 2 choses:

  • Tout d'abord les déclarations de "left" et "right" : la syntaxe nous indique ici que ces arguments peuvent être un Tree ou un entier !
  • Pour pouvoir traiter ces deux cas de figure de manière unifiée dans la classe, on a bien entendu besoin de faire converger ces deux cas de figure vers un type commun, en l'occurence un entire. C'est ce que fait la méthode resolve qui, à partir de deux types divergents renvoie un entier.

Si je suis bluffé par les unions de types, je le suis moins par la manière de faire reconverger ces types. Il semble que le switch (ou le "if") soit la seule option possible... On perd soudain l'élégance que l'on avait gagné.

On remarquera 2 autres points dans la méthode resolve:

  • La syntaxe du switch. Je suis un peu dérouté par le fait qu'i n'y ait pas d'accolades après le switch. Mais il semble que des accolades encadrant l'ensemble du switch soient par contre nécessaire. Le "else" à la fin des cases n'est nécessaire que si les différents cases offrent un ensemble disjoint.
  • La syntaxe "is" dans les différents cases permet d'utiliser directement x dans le type concerné sans qu'il soit nécessaire de le down-caster. En effet, on vient de tester qu'il est bien de ce type, donc pourquoi pas ? Je trouve ça sympa.

Un petit exercice pour nous avant le pose-pizza : écrire un opérateur binaire pour la multiplication.

class Multiply(Tree left, Tree right) extends BinaryOperator(left, right,
	function (Integer a, Integer b) a * b)
	{
    	}

Le langage est suffisemment évident pour que j'ai pu trouver la chose seul. Cela dit la syntaxe pour le passage de la fonction n'est pas évident. Le mot-clé "function" est optionnel mais me semble bien pour la clarté. Mais le fait de mettre le corps de la fonction sans accolades est carrément déstabilisant ! ##Break et second round Il est temps de se sustenter avant la dernière ligne droite.

ceylon2012-pause

Emmanuel Bernard en profite pour discuter avec les uns et les autres des premières impressions sur le langage.

ceylon2012-ebernard01

Deux tranches de pizza et une bière vite avalées, il est temps de se remettre au boulot.

ceylon2012-from-front

Sur ce nouveau round, nous allons construire un module pour parser le JSON que nous allons obtenir suite à un appel Rest et allons publier ce module sur un repository. Gloups !

L'IDE Eclipse nous facilite un peu la vie pour déclarer notre module. Un module contient de base deux fichiers : module.ceylon et package.ceylon.

module.ceylon va contenir les dépendances du module vers les autres modules, un peu comme le manifest OSGi. Voici le notre :

 module addinquy '1.0.0' {
	import ceylon.net '0.4';
	import ceylon.json '0.4';
} 

On a à la fois une déclaration de version de notre module (nommé "addinquy" en toute modestie) et les imports des modules requis dans leurs versions. On n'en sera toutefois pas quitte dans les classes quand il s'agira d'utiliser le contenu de ces packages.

package.ceylon contient le descripteur de package. C'est la loose, j'ai pas vraiment capté le concept. Je suis bon pour y revenir plus tard.

Maintenant il faut écrire le code du module lui-même.

void run() {
URI uri = parseURI("http://192.168.18.117:9000/api/1/search-modules");
	value response = uri.get().execute(); 
	value json = parse(response.contents);
	if (is JsonArray results = json["results"]) {
 		for (mod in results) {
 			if (is JsonObject mod) {
 				print(mod["module"]);
 			}
 		}
 	}
}

Comme on peut s'en douter les 2 premières lignes s'occupent de l'invocation du web service, tandis que la troisième transforme un objet http en objet JSON. Le reste du code récupère la partie "results" du JSON et va sortir uniquement les valeurs de "module" de ce JSON. En principe nous aurions pu utiliser les compréhensions pour faire cela de manière plus élégante. Bien que nous ayons mis les dépéndances vers ceylon.net et ceylon.json, on a quand même pas accès aux objets de manière magique. Des déclarations d'import sont nécessaires.

import ceylon.net.uri { parseURI, URI }
import ceylon.json{parse,
	JsonArray=Array,
	JsonObject=Object}

On voit 3 types de déclarations ici:

  • Un import direct de classe (URI) utilisé directement dans le code
  • Un import avec alias de type. Cela permet d'utiliser des classes ayant les mêmes noms dans deux modules différents en gérant la collision de nom. C'est pratique, même quand même un peu casse-gueule. Il faut faire attention !
  • Deux imports de fonctions (parse et parseURI), on l'a vu on peut déclarer au top level des variabes ou des fonctions. C'est le choix de Ceylon par rapport à Java qui exige que tout soit dans des classes. Cela permet à Ceylon de se passer des membres statiques dont on peut juger qu'ils sont conceptuellement questionnables.

L'appel à la fonction parse renvoie un objet qui peut être un JsonArray, un JsonObject ou un Integer. Pourtant on peut l'affecter à une variable. C'est que son type peut être inféré sur une union de type que l'on a construit juste avant via une déclaration d'alias.

alias Json = JsonArray|JsonObject|Integer;

Voilà, il reste encore une chose à faire : publier ce module !

##Ceylon Herd Ceylon Herd est le repository de modules de Ceylon. Bon je n'ai hélas pas pris de notes suffisemment détaillées pour retranscrire les manipulations ici, mais en gros les étapes sont les suivantes :

  1. S'enregistrer sur le repo Ceylon Herd. tant qu'on existe pas, on peut utiliser les modules, mais on ne peut pas les publier.
  2. Prétendre au projet. On postule et c'est l'administrateur du Herd qui accorde la propriété.
  3. Uploader et valider. Ceylon Herd fait des vérifications afin d'accorder la possibilité de publier.
  4. Publier. Le module est à la disposition de tous.

Aujourd'hui il y a un repository officiel Ceylon Herd officiel. Pour jouer avec un repository local, il faut cloner et déployer localement le repository. Il est disponible sur Github. Pour rendre l'accès plus facile au langage, JBoss devrait peut-être envisager de rendre possible la création de repo en PaaS !

##En conclusions Une bonne soirée, tout d'abord. Et je ne suis pas trop rouillé, ce qui est aussi une bonne nouvelle (pour moi). Il y a pas mal de trucs que j'aime bien dans le langage, à la fois plus rigoureux et plus flexible que Java. Nous n'avons pas abordé les conventions de nommage incluses dans le langage, ce que je trouve intelligent. Bien sûr la modularité présente dès le départ constitue une avancée interessante et coupe, par rapport à Java, un héritage encombrant. Toutefois tout cela ne suffira pas à faire partir les développeur Java vers Ceylon. Il manque réellement un outil ou un framework capable de créer un grand mouvement. Comme l'a été Rails pour Ruby, par exemple !

Je vais terminer cette introduction avec les j'aime / j'aime pas

###J'aime

  • Le concept de modules intégré et pensé dans le langage dès le départ.
  • Les conventions de nommage "built in" dans le langage. Même si on ne l'a pas abordé.
  • L'inférence de type qui évite les déclarations fastidieuses. Et le fait que celle-ci soit seulement statique pour rendre le langage plus solide et déterministe.
  • Les closures.
  • La flexibilité du langage n'obligeant pas à tout intégrer dans une classe et n'imposant pas la bijection fichier / classe.
  • Les unions de types. ça a de la gueule.
  • La déclaration "is" qui nous épargne le downcast !
  • L'idée de na pas limiter Ceylon à la JVM et de le faire tourner aussi sur la VM Javascript. L'idée de faire tourner Ceylon sur .NET est aussi dans les cartons, mais ce n'est pas forcément évident...

###J'aime pas

  • La syntaxe qui parait parfois contexte-dépendante, comme la nécessité ou pas de mettre "value" devant une déclaration, ou les brackets encadrant une expression ...
  • Pas de manière élégante de faire converger une union de type : le switch, c'est moche.
  • L'opérateur "..." pas encore bien clair pour moi.
  • Un interfaçage Maven visiblement balbutiant et en tout cas pas fantastiquement documenté. Mais bon pour être honnête, je n'ai rien testé !
  • A priori pas de module de tests unitaires officiel ? Je suis tombé sur le module de tests de Stéphane Epardaud, mais il semble un peu vieux. Je ne trouve pas trace d'un tel module dans le SDK et en tout cas aucune intégration Eclipse. Une lacune à combler.

###A penser pour l'avenir La release 1.0 va sortir, il est temps de penser à sortir de la cible "innovateurs" pour attaquer sérieusement les "early adopters". A mon avis, pour cela il faut :

  • De l'outillage de build solide et documenté. Maven est un bon client, pourquoi pas d'autres (Gradle, SBT) ? Il doit nécessairement permettre outre la construition d'archive .car, faire l'execution des tests unitaires et l'interfaçage avec Herd.
  • Un framework de tests unitaires, intégré dans l'IDE et aussi dans l'outillage de build.
  • Des tutoriaux. Il y en a déjà sur le site Ceylon, il en faut plus ! Idéalement un bouquin donnerait de la visibilité en plus de l'aspect didactique.

Après les "early adopters" il y a la "early majority" sur la courbe de Moore. Pour ceux-là, je vois surtout un point important :

  • Le framework de la mort-qui-tue ! Ce que Rails est à Ruby, Play! à Scala, etc... Bref le truc tellement bien qu'il donne envie de passer à Ceylon rien que pour l'utiliser !

##Next Nous n'avons fait qu'effleurer le langage, mais il fait bien envie. C'est maintenant que l'on va voir si j'ai du courage : celui d'essayer les autres fonctionnalités de Ceylon...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment