I. Avant-propos▲
Cet article est le troisième d'une série articulée sur la présentation du langage Ceylon :
- Présentation et installation ;
- Concepts de base ;
- Typage ;
- Appels et arguments ;
- Collections ;
- Modules ;
- 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 NullNull est un type à part entière et que nullnull 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 :
//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 :
//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.
//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.
//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 :
//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.
//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 :
//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 :
//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 :
//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 :
//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 :
//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.
//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 :
//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 :
//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 :
//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é :
//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 :
//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 :
//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 :
//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) :
//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.
//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);
}
}//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 :
//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 Objectceylon.language.Object, mais Anythingceylon.language.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 Javajava.util.List).
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 :
//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 :
//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 :
//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 :
//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 :
//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 :
//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 :
//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.
//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 :
//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 :
//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>.
//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 :
//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 « ObjectObject ».
//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 » (|) :
//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 :
- 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) ;
- 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) ;
- 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) ;
- Les types énumérés doivent explicitement étendre ou satisfaire le type énumérant ;
- 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 :
//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 :
//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 :
//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 :
//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 :
//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 :











