Tutoriel Ceylon : Typage

Logan Mauzaize

Nombreux sont les langages dédiés à la JVM, mais aucun autre que Ceylon ne propose un système de typage aussi poussé ainsi qu'une compilation en JavaScript. À travers une série d'articles, je propose de vous faire découvrir tous les mystères de ce langage conçu par Gavin King.

Commentez

- Titre : Tutoriel Ceylon : Typage

- Auteur : Logan Mauzaize

- Parution : 4 septembre 2014

- Mise à jour : 17 octobre 2014

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 Logan Mauzaize. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Avant-propos

- II. Null

- II-A. Optionnel

- II-B. Opérateurs

- III. Type

- III-A. Union

- III-B. Intersection

- III-C. Alias

- III-D. Inférence

- IV. Génériques

- IV-A. Définition

- IV-B. Covariance/Contravariance

- IV-C. Héritage

- IV-D. Union et intersection

- IV-E. Contraintes

- V. Énumération

- VI. Conclusion

- VII. Remerciements

- VIII. Annexes

- VIII-A. Sources des exemples

- VIII-B. Importer le projet sous Eclipse depuis Git

I - Avant-propos

Cet article est le troisième d'une série articulée sur la présentation du langage Ceylon :

1. Présentation et installation ;

2. Concepts de base ;

3. Typage ;

4. Appels et arguments ;

5. Collections ;

6. Modules ;

7. Interopérabilité avec Java.

Cet article propose de vous faire découvrir l'un des points forts du langage : son système de typage.

II - Null

Le premier concept à assimiler en Ceylon, c'est que Null est un type à part entière et que null en est simplement le singleton. Cependant Ceylon offre plusieurs sucres syntaxiques pour manipuler les variables qui dépendent de ce type.

II-A - Optionnel

Le premier sucre syntaxique à connaître c'est la notation postfixée ?. Il s'agit simplement d'indiquer qu'une valeur est « optionnelle ». La variable peut être soit du type indiqué, soit du type Null :

Optionnel

//Source: tutoriel/b_type/a_null/a_optionnel.ceylon
String? optionnel(Boolean set) {
if (set) {
return "Une chaîne";
} else {
return null;
}
}

void demoOptionnel() {
String? nonNull = optionnel(true);
String? null = optionnel(false);
print(nonNull);
print(null);
}

Une construction intéressante offerte par Ceylon, c'est le chaînage d'appel. Il arrive parfois que l'on ait besoin de traverser un graphe d'objet, mais que potentiellement, tout ou partie des références à traverser puissent être null. Si l'on veut simplement avoir la valeur null lorsque l'une des références est null, il suffit de l'indiquer à Ceylon :

Graphe optionnel

//Source: tutoriel/b_type/a_null/b_grapheOptionnel.ceylon
// Pseudo-graphe dont les éléments impairs sont optionnels.
class OptionnelA(shared OptionnelB b) {}
class OptionnelB(shared OptionnelC? c) {}
class OptionnelC(shared OptionnelD d) {}
class OptionnelD(shared OptionnelE? e) {}
class OptionnelE(shared String? f) {}

void afficherOptionnelA(OptionnelA? a) {
print(a?.b?.c?.d?.e?.f);
}

void demoGrapheOptionnel() {
afficherOptionnelA(null);
afficherOptionnelA(OptionnelA(OptionnelB(null)));
afficherOptionnelA(OptionnelA(OptionnelB(OptionnelC(OptionnelD(null)))));
afficherOptionnelA(OptionnelA(OptionnelB(OptionnelC(OptionnelD(OptionnelE(null))))));
afficherOptionnelA(OptionnelA(OptionnelB(OptionnelC(OptionnelD(OptionnelE("Résultat"))))));
}

II-B - Opérateurs

Maintenant que nous pouvons déclarer des références optionnelles, il serait bien de voir comment les manipuler correctement. Le premier couple d'opérateurs est l'un de ceux que l'on a déjà vus lors de l'apprentissage des bases : then-else. else permet soit de récupérer l'opérande de gauche s'il n'est pas null, sinon l'opérande de droite.

Opérateur else

//Source: tutoriel/b_type/a_null/c_else.ceylon
void demoNullElse() {
String? a = null;
String? b = "Une valeur";
String? c = "Une autre valeur";
print(a else b);
print(b else c);
}

L'opérande gauche de l'opérateur then est une expression logique. Si elle est vraie, l'opérateur retourne l'opérande de droite, sinon la valeur null.

Opérateur then

//Source: tutoriel/b_type/a_null/d_then.ceylon
void demoNullThen() {
String a = "Une valeur";
String b = "Une autre valeur";
print(true  then a);
print(false then b);
}

Ainsi le but de l'opérateur else est d'empêcher les valeurs null. Tandis qu'au contraire, l'opérateur then produit des valeurs null. Il conviendra donc de les coupler pour éviter les valeurs null :

Couplage des opérateurs else-then

//Source: tutoriel/b_type/a_null/e_elsethen.ceylon
void afficherNullElseThen(Float n) {
String signe = (n == infinity then "infini")
else (n < 0.0 then "négatif")
else (n > 0.0 then "positif")
else "zéro";
print(signe);
}

void demoNullElseThen() {
afficherNullElseThen(infinity);
afficherNullElseThen(-1.0);
afficherNullElseThen(1.0);
afficherNullElseThen(0.0);
}

Un autre opérateur intéressant est exists. Celui-ci renvoie vrai si la référence n'a pas la valeur null. Suite à cela, Ceylon assume que la référence ne peut être null soit dans le bloc pour les structures de contrôles (ex. if, while), soit pour le restant du bloc avec l'instruction assert. On peut ainsi appeler les méthodes d'un type optionnel. L'opérateur peut également être combiné avec une déclaration s'il est utilisé avec une structure de contrôles.

Opérateur exists

//Source: tutoriel/b_type/a_null/f_exists.ceylon
class ClassExists() {
shared void demoIf(String? chaine) {
if (exists chaine) {
print("demoIf : " + chaine.uppercased);
} else {
print("demoIf : NULL");
}
}
shared void demoDeclaration(OptionnelA? a) {
if (exists f = a?.b?.c?.d?.e?.f) {
print("demoDeclaration : " + f);
}
}
shared void demoAssert(String? chaine) {
try {
assert (exists chaine);
print("demoAssert : " + chaine.reversed);
} catch (AssertionException e) {
print("demoAssert : " + e.message);
}
}
}

void demoExists() {
ClassExists demo = ClassExists();
demo.demoIf("fo");
demo.demoIf(null);
demo.demoDeclaration(OptionnelA(OptionnelB(null)));
demo.demoDeclaration(OptionnelA(OptionnelB(OptionnelC(OptionnelD(OptionnelE("ob"))))));
demo.demoAssert("ar");
demo.demoAssert(null);
}

III - Type

III-A - Union

L'union de types consiste simplement à indiquer que l'on ne connaît pas le type exact d'une référence, mais que l'on sait qu'il appartient à un ensemble limité. Par exemple, la référence suivante peut être soit une chaîne de caractères, soit un nombre :

Union

//Source: tutoriel/b_type/a_union/a_union.ceylon
variable String|Integer variableUnion = 1;

L'union de types est un concept que nous avons déjà manipulé, mais de manière masquée grâce au sucre syntaxique. En effet, la notation postfixée ? est simplement un raccourci pour indiquer l'union du type déclaré et le type Null. Ainsi la déclaration String? est un raccourci pour String|Null.

De base, Ceylon autorise l'utilisation des méthodes de tous les types communs (classe ancêtre commune, mais également les interfaces. Ainsi dans l'exemple ci-dessous, il est possible d'appeler la méthode afficherA bien que nous ne sachions pas quel est le type exact de la référence :

Union et ancêtre commun

//Source: tutoriel/b_type/a_union/b_ancetre.ceylon
interface UnionZ {
shared default void afficherZ() {
print("UnionZ");
}
}

class UnionA() {
shared void afficherA() {
print("UnionA");
}
}

class UnionB() extends UnionA() satisfies UnionZ {
shared void afficher() {
print("UnionB");
}
}

class UnionC() extends UnionA() satisfies UnionZ {
shared void afficher() {
print("UnionC");
}
}

class UnionD() extends UnionA() {
shared void afficher() {
print("UnionC");
}
}

void demoUnion() {
UnionB|UnionC union = UnionB();
union.afficherA();
union.afficherZ();
}

Pour restreindre une union de types, il faut utiliser l'opérateur is. Comme pour l'opérateur exists, is peut être utilisé dans un if ou un assert et il peut également être accompagné d'une déclaration :

Opérateur is

//Source: tutoriel/b_type/a_union/c_is.ceylon
void demoIsIf() {
if (variableUnion is String) {
print("C'est une chaîne");
} else if (variableUnion is Integer) {
print("C'est un entier");
}
}

void demoIsDeclaration() {
if (is String variable = variableUnion) {
print(variable.uppercased);
} else if (is Integer variable = variableUnion) {
print(variable + 1);
}
}

void demoIsAssert() {
assert (is Integer variable = variableUnion);
print(variable - 1);
}

Il est également possible d'utiliser l'instruction switch avec l'opérateur is :

Union, is et switch

//Source: tutoriel/b_type/a_union/d_is_switch.ceylon
void demoIsSwitch() {
String|Integer ref = variableUnion;
switch (ref)
case (is String) {
print(ref.uppercased);
}
case (is Integer) {
print(ref + 1);
}
}

On remarquera dans ce dernier exemple que la référence a dû être « stockée » dans une référence constante avant d'être testée dans le switch afin d'éviter les problèmes de concurrence. Si variableUnion est changé, le type restera bien valide dans les différents blocs du switch.

Pour terminer la présentation des unions de types, je vais vous parler de ce à quoi ils peuvent servir en dehors des références optionnelles. Ceylon n'autorisant pas la surcharge des méthodes et notamment des constructeurs, les unions de types permettent d'autoriser différents types d'arguments. Charge à vous ensuite de distinguer les différents cas :

Union et surcharge

//Source: tutoriel/b_type/a_union/e_surcharge.ceylon
class ClassUnion(String|Integer soi) {
shared String s;
shared Integer i;

if (is String ts = soi)       { s = ts; i = 0;  }
else if (is Integer ti = soi) { s = ""; i = ti; }
else                          { s = ""; i = 0;  }

shared actual String string => "``s``;``i``";

void afficherString(String s) {
print("afficherString  : " + s);
}
void afficherInteger(Integer i) {
print("afficherInteger : ``i``");
}
shared void afficher(String|Integer soi) {
switch (soi)
case (is String)  { afficherString(soi);  }
case (is Integer) { afficherInteger(soi); }
}
}

void demoClassUnion() {
ClassUnion s = ClassUnion("foo");
ClassUnion i = ClassUnion(1);
print(s);
print(i);
s.afficher("bar");
s.afficher(2);
}

III-B - Intersection

Si l'union de types correspond à l'opérateur logique « ou », l'intersection correspond au « et ». En effet, la référence doit correspondre à TOUS les types mentionnés.

Intersection

//Source: tutoriel/b_type/b_intersection/a_intersection.ceylon
interface IntersectionA {
shared default void afficherA() {
print("IntersectionA");
}
}

interface IntersectionB {
shared default void afficherB() {
print("IntersectionB");
}
}

class IntersectionAB() satisfies IntersectionA&IntersectionB {}

void demoIntersection() {
IntersectionAB ab = IntersectionAB();
ab.afficherA();
ab.afficherB();
}

Ceci permet de gérer des hiérarchies de classes distinctes, mais qui satisfont les mêmes interfaces. L'exemple le plus parlant est le cas des classes StringBuilder et StringBuffer de Java. Leur ancêtre commun est la classe Object, cependant elles implémentent toutes deux les interfaces CharSequence et Appendable :

Intersection - Exemple en Java

//Source: tutoriel/b_type/b_intersection/DemoAppendableCharSequence.java
public class DemoAppendableCharSequence {

public static <T extends Appendable&CharSequence> void affiche(T appendableCharSequence) throws IOException {
if (appendableCharSequence.length() == 0) {
appendableCharSequence.append("<empty>");
}
System.out.println(appendableCharSequence);
}

public static void main(String[] args) throws IOException {
System.out.println(StringBuilder.class);
affiche(new StringBuilder());
System.out.println(StringBuffer.class);
affiche(new StringBuffer());
}
}

On constate ainsi que l'intersection permet également de contourner la surcharge des méthodes. Plutôt que d'écrire une méthode pour chaque type qui vous intéresse, vous pouvez utiliser une seule méthode qui dépend uniquement d'une liste d'interfaces.

Un point intéressant à noter est l'utilisation de l'opérateur is :

Intersection et is

//Source: tutoriel/b_type/b_intersection/b_is.ceylon
void demoIntersectionIs() {
IntersectionA a = IntersectionAB();
a.afficherA();
if (is IntersectionB a) {
//Ici le type de a est IntersectionA&IntersectionB
a.afficherA();
a.afficherB();
}
}

III-C - Alias

Les alias et l'inférence de type (que nous aborderons par la suite) permettent tous deux de réduire la verbosité. Les alias offrent l'ajout de sémantique au code. Ainsi Gens n'est qu'un ensemble de Personne :

Gens ou ensemble de personnes ?

//Source: tutoriel/b_type/c_alias/a_gens.ceylon
interface Gens => Set<Personne>;

Vous aurez conclu de cet exemple que pour définir un alias, il suffit de préciser le type et le nom suivis d'une « grosse flèche » (fat arrow), et enfin sa définition.

Utiliser un alias n'empêche pas d'utiliser le type désigné :

Alias ou type désigné

//Source: tutoriel/b_type/c_alias/b_typeDesigne.ceylon
void demoAliasGensAffiche(Gens gens) {
print(gens.size);
}
void demoAliasGens() {
Set<Personne> gens = LazySet({Personne("Jean", "DUPONT"), Personne("Christine", "DURAND")});
demoAliasGensAffiche(gens);
}

Si vous définissez un alias sur une classe, il faut déclarer les paramètres :

Alias de classe avec paramètre

//Source: tutoriel/b_type/c_alias/c_parametre.ceylon
class ConstructeurAvecParametre(shared String unAttribut) {}
class AliasAvecParametre(String parametre) => ConstructeurAvecParametre(parametre);

Il est également possible de créer des alias correspondant à des unions, ou des intersections de types à l'aide du mot-clé alias :

Alias d'union

//Source: tutoriel/b_type/c_alias/d_union.ceylon
class A() {}
class B() {}
alias AliasUnionAB => A|B;

Il est possible d'étendre ou de satisfaire un alias à condition que celui-ci ne soit pas déclaré en utilisant le mot-clé alias :

Étendre/Satisfaire un alias

//Source: tutoriel/b_type/c_alias/e_etendre_satisfaire.ceylon
// Déclaration valide
interface C {
shared formal String nom;
}
interface D {}
interface AliasC => C;
class AliasClassAvecParametre() extends AliasAvecParametre("a") satisfies C&D {
shared actual String nom => unAttribut;
}

// Déclaration non valide
alias AliasCetD => C&D;
class AliasClassCetD() satisfies AliasCetD {}

Pour finir, un alias peut être générique et avoir des contraintes de types (que nous aborderons plus loin) :

Alias générique

//Source: tutoriel/b_type/c_alias/f_generique.ceylon
class Named<Value>(String name, Value val)
given Value satisfies Object
=> Entry<String,Value>(name,val);

III-D - Inférence

L'inférence de type dans Ceylon consiste simplement à omettre le type, d'une référence ou de retour d'une fonction, si :

- celle-ci (la référence ou la fonction) n'est pas exposée (Note : si l'élément est exposé, le langage suppose que cela fait alors partie d'une API) :

- la déclaration ne peut pas être un élément de haut niveau,

- la déclaration ne peut pas être partagée (shared),

- il n'est pas possible d'inférer le type des paramètres ;

- le type peut être déduit dès la déclaration, cela signifie que :

- la déclaration ne peut pas être abstraite,

- la référence doit être initialisée lors de sa déclaration.

Si vous remplissez ces conditions, il suffit de remplacer le type par  value pour les références et function pour les fonctions.

Inférences valides

//Source: tutoriel/b_type/d_inference/a_valide.ceylon
class Personne(shared String prenom, shared String nomFamille) {}
void demoInferenceValue() {
value jeanDupont = Personne("Jean", "DUPONT");
print(jeanDupont.nomFamille);
}

class DemoInferenceFunction() {
function somme(Integer a, Integer b) {
value resultat = a + b;
return resultat;
}
shared void affiche(Integer a, Integer b) {
print(somme(a,b).sign);
}
}

Inférences non valides

//Source: tutoriel/b_type/d_inference/b_nonvalide.ceylon
// Déclarations de haut niveau
value demoInferenceReferenceDeHautNiveau = "a";
function demoInferenceFonctionDeHautNiveau(Integer a, Integer b) => a+b;

// Déclarations partagées
class DemoInferenceReferencePartagee() {
shared value reference = "a";
}
class DemoInferenceFonctionPartagee() {
shared function fonction() {
return print("a");
}
}

// Paramètre
class DemoInteferenceParametre() {
void fonction(value a) {
print(a);
}
}

// Déclarations abstraites
abstract class DemoInferenceReferenceAbstraite() {
formal value reference;
}
abstract class DemoInferenceFonctionAbstraite() {
formal function fonction();
}

// Initialisation retardée
void demoInferenceInitialisation() {
value reference;
reference = "a";
}

N'allez pas imaginer que les déclarations ne sont pas typées ! C'est le compilateur qui va s'en charger selon un algorithme très simple. Pour les références, il s'agit du type de l'expression que vous assignez. Pour les accesseurs et les fonctions, il s'agit de l'union des types retournés.

IV - Génériques

IV-A - Définition

La généricité est la possibilité d'introduire une forme d'abstraction supplémentaire à vos types (classe ou interface). En effet, cela permet de décrire des comportements pouvant exploiter différents types de données.
Pour être plus concret, prenons l'exemple d'une liste. Lors de la définition du type Liste, on ne connaît pas les types qui seront manipulés. Cependant on est en mesure de définir un tel type de manière générique :

Liste générique

//Source: tutoriel/c_generiques/a_definition/a_liste.ceylon
"Spécification naïve d'une liste"
shared interface Liste<Element> {
shared formal void add(Element element);
shared formal Element get(Integer index);
shared formal Integer size;
}

"Implémentation naïve d'une liste
Celle-ci contient une et une seule valeur"
shared class Singleton<Type>(variable Type element) satisfies Liste<Type> {
shared actual void add(Type newElement) {
Boolean unsupported = false;
assert (unsupported);
}

shared actual Type get(Integer index) {
assert(index == 0);
return element;
}

shared actual Integer size = 1;

shared actual String string {
if (is Object obj = element) {
return "{``obj``}";
}
return "{}";
}
}

void demoGeneriqueListe() {
Liste<Integer> unEntier = Singleton<Integer>(0);
print(unEntier);
Liste<String> uneChaine = Singleton<String>("une chaîne");
print(uneChaine);
}

La syntaxe suit les conventions d'usage. Une petite particularité par rapport à Java, le type racine n'est pas Object, mais Anything ! Toujours pour se distinguer de Java, la convention dans Ceylon est d'utiliser des noms significatifs (ex. « Element ») pour les paramètres de type plutôt qu'une simple lettre (par exemple « E » et List en Java).

De la même manière qu'il est possible de définir une valeur par défaut à une fonction, il est également possible de définir un type par défaut à un type générique :

Générique avec un type par défaut

//Source: tutoriel/c_generiques/a_definition/b_defaut.ceylon
class GenericDefaut<PremierType,SecondType=Null>() {
shared void afficherPremier(PremierType premier) {
String valeur;
if (exists premier) { valeur = premier.string; }
else                { valeur = "";             }
print("premier=``valeur``");
}
shared void afficherSecond(SecondType second) {
String valeur;
if (exists second) { valeur = second.string; }
else               { valeur = "";            }
print("second=``valeur``");
}
}

void demoGenericDefaut() {
GenericDefaut<String> chaine = GenericDefaut<String>();
chaine.afficherPremier("bonjour");
chaine.afficherSecond(null);
}

Les paramètres de type sont obligatoires, ainsi l'écriture suivante n'est pas correcte :

Paramètre de type obligatoire

//Source: tutoriel/c_generiques/a_definition/c_obligatoire.ceylon
GenericDefaut parametreDeTypeAbsent = GenericDefaut<String>();

Cependant cette écriture est acceptée si tous les paramètres de type ont une valeur par défaut :

Paramètre de type facultatif

//Source: tutoriel/c_generiques/a_definition/d_facultatif.ceylon
class GeneriqueTypeOptionnel<PremierType=String>() {}
GeneriqueTypeOptionnel generiqueTypeOptionnel = GeneriqueTypeOptionnel<String>();

Si définir les types lors de la déclaration vous ennuie, pensez à utiliser le mot-clé value !

Si les paramètres de type sont obligatoires pour les déclarations de type, ils sont toutefois le plus souvent optionnels lors de l'invocation de méthodes ou d'instanciation :

Paramètre de type et invocation

//Source: tutoriel/c_generiques/a_definition/e_invocation.ceylon
class DemoGeneriqueTypeImplicite<Contenu>(shared Contenu contenu) {
shared void affiche() {
print(contenu);
}
shared void afficheAutre<AutreContenu>(AutreContenu autre) {
print(autre);
}
}

void demoGeneriqueTypeImplicite() {
value generique = DemoGeneriqueTypeImplicite("String");
generique.affiche();
generique.afficheAutre(1);
}

IV-B - Covariance/Contravariance

Passons désormais à un principe plus complexe lié à l'introduction des génériques : covariance/contravariance.
Derrière ces mots barbares se cache la possibilité d'utiliser des types génériques avec des paramètres « compatibles ». Conceptuellement, on peut admettre qu'une liste de moutons est une liste d'animaux. En réalité, ce n'est pas strictement vrai, puisqu'on ne pourrait pas ajouter de loup dans cette liste :

Mélange de genre

//Source: tutoriel/c_generiques/b_variance/a_melange.ceylon
import tutoriel.c_generiques.a_definition { Liste, Singleton }

interface Animal {
shared default actual String string => "Animal";
}
interface Mouton satisfies Animal {
shared default actual String string => super.string + "/Mouton";
}
class Dorset() satisfies Mouton {
shared default actual String string => super.string + "/Dorset";
}
class Soay() satisfies Mouton {
shared default actual String string => super.string + "/Soay";
}
class Loup() satisfies Animal {
shared default actual String string => super.string + "/Loup";
}

void demoVarianceInvalide() {
Liste<Mouton> bergerie      = Singleton<Mouton>(Dorset());
Liste<Dorset> cheptelDorset = bergerie;  // Et si j'avais une Soay ?
Liste<Animal> refuge        = bergerie;  // Supposé bon
refuge.add(Loup());                         // Un loup dans la bergerie !
}

Bien entendu le compilateur refusera d'affecter bergerie à refuge ou cheptel. On dit alors que Liste est invariante en Element. Nous allons donc séparer les méthodes qui produisent de celles qui consomment :

Producteur / Consommateur

//Source: tutoriel/c_generiques/b_variance/b_producteur_consommateur.ceylon
shared interface Producteur<out Produit> {
shared formal Produit produire();
}

shared interface Consommateur<in Consommable> {
shared formal void consommer(Consommable p);
}

- l'annotation out est utilisée pour indiquer que Producteur est covariant en Produit. Ses méthodes ne font que produire (retourner) des Produit mais n'en consomment (prennent en paramètre) jamais ;

- l'annotation in est utilisée pour indiquer que Consommateur est contravariant en Consommable. Ses méthodes ne font que consommer (prendre en paramètre) des Consommable mais n'en produisent (retournent) jamais.

Le compilateur vérifiera si la déclaration in/out des paramètres de type est correcte avec leur utilisation. Ainsi les définitions suivantes ne sont pas valides :

Covariance / Contravariance non respectées

//Source: tutoriel/c_generiques/b_variance/c_non_valide.ceylon
interface ProducteurInvalide<out Produit> {
shared formal void transformer(Produit produit);
}

interface ConsommateurInvalid<in Consommable> {
shared formal Consommable jeter();
}

Et maintenant ? Puisque le compilateur est sûr de l'utilisation qui est faite des paramètres de type, il nous autorise à considérer :

- un éleveur de moutons comme un éleveur d'animaux. L'espèce qu'il produira sera le mouton et donc un animal. En revanche, il ne pourra pas accepter n'importe quel animal ;

- un loup comme un mangeur de moutons. Il peut manger toute sorte de moutons, mais pas les autres animaux.

Covariance / Contravariance

//Source: tutoriel/c_generiques/b_variance/d_valide.ceylon
void demoVariance() {
object eleveurOvin satisfies Producteur<Mouton> {
shared actual Mouton produire() {
return Dorset();
}
}
Producteur<Animal> eleveur = eleveurOvin;
print("L'éleveur produit des ``eleveur.produire()``");

object loup satisfies Consommateur<Mouton> {
shared actual void consommer(Mouton mouton) {
print("Miam ! Je mange du ``mouton``");
}
}
Consommateur<Dorset> gourmet = loup;
gourmet.consommer(Dorset());
}

IV-C - Héritage

En Java, un type ne peut pas hériter d'un autre plus d'une fois, et ce même si les paramètres de type diffèrent :

Java - Héritage avec différents paramètres de type

//Source: tutoriel/c_generiques/c_heritage/a_java.java
interface Liste<T> {}
class ListeDObject implements Liste<Object> {}
class ListeDeChaine extends ListeDObject implements Liste<String> {}
//The interface Liste cannot be implemented more than once with different arguments: Liste<Object> and Liste<String>

Tout cela est possible en Ceylon si, et seulement si le type n'est pas invariant en l'un de ses paramètres :

Ceylon - Héritage avec différents paramètres de type

//Source: tutoriel/c_generiques/c_heritage/b_ceylon.ceylon
class A() {}
class B() {}

interface HeritageAvecDifferentType<out Element> {}
interface HeritageAvecDifferentTypeA satisfies HeritageAvecDifferentType<A> {}
interface HeritageAvecDifferentTypeB satisfies HeritageAvecDifferentType<B> {}
class HeritageAvecDifferentTypeAetB() satisfies HeritageAvecDifferentTypeA & HeritageAvecDifferentTypeB {}

Il est intéressant de noter que cette dernière classe est compatible avec HeritageAvecDifferentType<A&B>.

Covariance - Héritage avec différents paramètres de type

//Source: tutoriel/c_generiques/c_heritage/c_covariance.ceylon
void demoHeritageAvecDifferentType() {
HeritageAvecDifferentType<A&B> ref = HeritageAvecDifferentTypeAetB();
}

Si les producteurs sont covariants en l'intersection des types, les consommateurs sont contravariants en l'union des types :

Contravariance - Héritage avec différents paramètres de type

//Source: tutoriel/c_generiques/c_heritage/d_contravariance.ceylon
import tutoriel.c_generiques.b_variance { Consommateur }

interface ConsommateurA satisfies Consommateur<A> {}
interface ConsommateurB satisfies Consommateur<B> {}
class ConsommateurAetB() satisfies ConsommateurA & ConsommateurB {
shared actual void consommer(A|B ab) {
if (is A a = ab) { print("A=``a``"); }
if (is B b = ab) { print("B=``b``"); }
}
}

IV-D - Union et intersection

Pour aller plus avant avec l'héritage, les unions et intersections, voici quelques propriétés à retenir pour éviter d'être perdu :

- pour les types covariants (« producteurs ») :

- Producteur<X>|Producteur<Y> est un sous-type de Producteur<X|Y>,

- Producteur<X>&Producteur<Y> est un super-type de Producteur<X&Y> ;

- pour les types contravariants (« consommateurs ») :

- Consommateur<X>|Consommateur<Y> est un sous-type de Consommateur<X&Y>,

- Consommateur<X>&Consommateur<Y> est un super-type de Consommateur<X|Y>.

IV-E - Contraintes

Bien que nous ayons la possibilité de définir des types et des fonctions génériques, il est souhaitable d'avoir un minimum de suppositions concernant les types manipulés. Par exemple, un ensemble n'a de sens que si l'égalité est définie. Or, celle-ci est définie à partir du type « Object ».

Contrainte de type

//Source: tutoriel/c_generiques/e_contraintes/a_type.ceylon
class Ensemble<out Element>(Element element) given Element satisfies Object {
shared Boolean contient(Object test) {
return element == test;
}
}

V - Énumération

Les types d'un langage comme Ceylon peuvent être vus comme des ensembles dont les éléments sont les instances. Les énumérations de Ceylon permettent de définir des ensembles disjoints.

Les énumérations sont toutes introduites à l'aide du mot-clé of suivi de l'ensemble des sous-types séparés par un « pipe » (|) :

Déclaration

//Source: tutoriel/d_enumeration/a_declaration.ceylon
interface A4Roues {}
class Vehicule() {}
abstract class VehiculeA4Roues() of Voiture|Camion extends Vehicule() satisfies A4Roues {}
class Voiture() extends VehiculeA4Roues() {}
abstract class Camion() extends VehiculeA4Roues() {}
class Semi() extends Camion() {}

Voici quelques règles au sujet des énumérations :

1. Le type énumérant (i.e. VehiculeA4Roues) doit être abstrait (abstract class ou interface) afin de s'assurer que les instances appartiennent nécessairement à l'une des classes filles (ensembles disjoints) ;

2. Le type énumérant peut implémenter des interfaces (i.e. A4Roues) et/ou hériter d'une classe abstraite ou non (i.e. Vehicule) ;

3. Les types énumérés (i.e. Voiture|Camion) doivent tous exister dans le même module (mais pas nécessairement le même package) ;

4. Les types énumérés doivent explicitement étendre ou satisfaire le type énumérant ;

5. Un type énuméré peut être abstrait et avoir un nombre indéfini de types enfants.

Les énumérations sont nécessaires aux conditions d'un switch. En effet, dans ce dernier tous les cas doivent être disjoints, le débranchement ne doit pas tenir compte de l'ordre des cas (contrairement à une succession de if). Sur une hiérarchie de classes, il n'y a aucun souci, car le système de type connaît la relation entre deux classes :

Switch sur classes non-énumérées

//Source: tutoriel/d_enumeration/b_switch_classes.ceylon
class Racine() {}
class Enfant1() {}
class Enfant2() {}
class Feuille1() extends Racine() {}
class Feuille2() extends Enfant1() {}
class Feuille3() extends Enfant1() {}
class Feuille4() extends Enfant2() {}
class PetitEnfant() extends Enfant2() {}
class Feuille5() extends PetitEnfant() {}
class Feuille6() extends Enfant2() {}

void switch_sur_classes() {
{Object+} liste = {
Racine(), Enfant1(), Enfant2(), Feuille1(), Feuille2(), Feuille3(),
Feuille4(), PetitEnfant(), Feuille5(), Feuille6()
};
for (Object objet in liste) {
String type;
switch (objet)
// case (is Racine) {}
case (is Enfant1)  { type = "le premier enfant";    }
// case (is Enfant2) {}
case (is Feuille1) { type = "la première feuille";  }
// case (is Feuille2) {}
// case (is Feuille3) {}
case (is Feuille4) { type = "la sixième feuille";   }
// case (is PetitEnfant) {}
case (is Feuille5) { type = "la cinquième feuille"; }
case (is Feuille6) { type = "la sixième feuille";   }
else               { type = "un inconnu";           }
print("Je suis ``type``");
}
}

En revanche, pour les interfaces ce n'est pas possible, car elles ne forment pas normalement de hiérarchie. L'utilisation de l'énumération devient alors obligatoire :

Switch sur interfaces

//Source: tutoriel/d_enumeration/c_switch_interfaces.ceylon
interface Noeud of Fichier | Repertoire | Lien {
shared formal String nom;
}
interface Fichier    satisfies Noeud {}
interface Repertoire satisfies Noeud {
shared formal {Noeud*} noeuds;
}
interface Lien       satisfies Noeud {
shared formal Noeud cible;
}

class FichierImpl   (shared actual String nom                              ) satisfies Fichier {}
class RepertoireImpl(shared actual String nom, shared actual Noeud[] noeuds) satisfies Repertoire {}
class LienImpl      (shared actual String nom, shared actual Noeud cible   ) satisfies Lien {}

"
racine
noeud_1/
noeud_1_1
noeud_1_2
noeud_1_3/
noeud_2/
noeud_2_1 -> noeud_1
"
void switch_sur_interfaces() {
Noeud noeud_1_1 = FichierImpl("noeud_1_1");
Noeud noeud_1_2 = FichierImpl("noeud_1_2");
Noeud noeud_1_3 = RepertoireImpl("noeud_1_3", []);
Noeud noeud_1   = RepertoireImpl("noeud_1", [noeud_1_1, noeud_1_2, noeud_1_3]);
Noeud noeud_2_1 = LienImpl("noeud_2_1", noeud_1);
Noeud noeud_2   = RepertoireImpl("noeud_2", [noeud_2_1]);
Noeud racine    = RepertoireImpl("racine", [noeud_1, noeud_2]);
parcourir("", racine);
}

void parcourir(String prefixe, Noeud courant) {
switch (courant)
case (is Fichier) {
print(prefixe + courant.nom);
}
case (is Repertoire) {
print(prefixe + courant.nom + "/");
String sousPrefixe = prefixe + "  ";
for (Noeud noeud in courant.noeuds) {
parcourir(sousPrefixe, noeud);
}
}
case (is Lien) {
print(prefixe + courant.nom + " -> " + courant.cible.nom);
}
else {}
}

Bien entendu, il ne sera pas possible de satisfaire deux interfaces énumérées :

Satisfaire deux interfaces énumérées

//Source: tutoriel/d_enumeration/d_satisfaires_enumerees.ceylon
// Erreurs de compilation
class FichierRepertoire1(shared actual String nom, shared actual Noeud[] noeuds) satisfies Fichier&Repertoire {}
class FichierRepertoire2(String nom, shared actual Noeud[] noeuds) extends FichierImpl(nom) satisfies Repertoire {}

Désormais on est en droit de se demander comment écrire une énumération de valeurs comme en Java ? Eh bien, en faisant une énumération d'objets :

Énumération d'objets

//Source: tutoriel/d_enumeration/e_enum_objets.ceylon
class Forme() {}
abstract class FormeSimple() of carre | rond | triangle extends Forme() {}
object carre    extends FormeSimple() {}
object rond     extends FormeSimple() {}
object triangle extends FormeSimple() {}

Cerise sur le gâteau, contrairement à Java, vos énumérations Ceylon peuvent étendre une classe !

Lorsque vous utilisez des objets dans un switch, la syntaxe est un peu différente. En effet le mot-clé is est omis :

Switch sur objets

//Source: tutoriel/d_enumeration/f_switch_objets.ceylon
abstract class FormeComplexe() of Chemin | Polygone extends Forme() {}
class Chemin()   extends FormeComplexe() {}
class Polygone() extends FormeComplexe() {}

void switchObjets() {
Forme forme = carre;
switch (forme)
case (carre, triangle)    { print("J'ai des angles");                           }
case (rond)               { print("Je n'ai pas d'angles");                      }
case (is Chemin|Polygone) { print("J'ai trop de complexes");                    }
else                      { print("Angle ou pas angle, telle est la question"); }
}

Avant d'en terminer avec les énumérations, je souhaitais indiquer qu'en plus de fiabiliser votre code, elles permettent de protéger vos API en empêchant les utilisateurs de créer de nouvelles sous-classes.

VI - Conclusion

Désormais vous devriez être en mesure d'exploiter au mieux les capacités du typage de Ceylon. La prochaine partie est consacrée à la gestion des appels et des arguments.

VII - Remerciements

Je remercie toute l'équipe Ceylon pour la réalisation de ce nouveau langage, ainsi que pour leur disponibilité et leur patience pour répondre aux remarques et questions qu'on leur soumet.

Je remercie également Mickael Baron, Yann Caron alias CyaNnOrangehead, Thierry Leriche-Dessirier alias thierryler, bredelet, Jacques Théry alias jacques_jean et Claude Leloup pour leur relecture attentive, leurs remarques et leurs bons conseils.

Je tiens aussi à remercier la communauté Developpez.com qui a mis en place tous les outils, les procédures et l'hébergement nécessaires à la publication de cet article.

Enfin mon épouse et mes enfants pour leur patience et leur tolérance durant les nombreuses heures qui ont été nécessaires à la rédaction de cet article.

VIII - Annexes

VIII-A - Sources des exemples

Tous les exemples donnés dans cet article sont disponibles sous GitHub dans le répertoire src-03-typage.

VIII-B - Importer le projet sous Eclipse depuis Git

Pour importer le projet sous Eclipse, ouvrez la perspective « Git Repository Exploring » :

Copiez l'URL https://github.com/loganmzz/ceylon-articles.git, assurez-vous que la vue active est « Git Repositories » (en cliquant sur l'onglet, par exemple) :

Appuyez simplement sur la combinaison de touche « CTRL+V » pour faire apparaître la fenêtre « Clone Git Repository » :

Après avoir appuyé sur « Next > », la fenêtre de sélection des branches apparaît. Sélectionnez uniquement « officiel » :

Appuyez sur « Next > » pour faire apparaître la fenêtre de configuration du stockage local. Adaptez le chemin selon vos préférences, puis vérifiez que la branche est bien « officiel » et activez l'import des projets existants :

Il n'y a plus qu'à terminer en cliquant sur « Finish ». Le dépôt est cloné, puis le projet « ceylon-articles » est importé dans l'espace de travail :

Retour à l'article