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 :