Introduction Scala
Introduction Scala
Introduction Scala
Introduction à Scala
Emile Contal & Nathan Grosshans & Mathieu Hilaire
& Juraj Kolčák & Amélie Ledein & Stefan Schwoon
29 janvier 2021
5 Programmation générique 12
5.1 Généricité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
5.2 Types bornés . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
5.3 Bornes multiples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
6 Interfaces Graphiques 14
6.1 Une première fenêtre et ses composants . . . . . . . . . . . . . . . . . . . . . . . 14
6.2 Réagir aux actions de l’utilisateur . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
6.2.1 Avec listenTo et reactions . . . . . . . . . . . . . . . . . . . . . . . . . 15
6.2.2 Avec Action . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
6.3 Personnalisations graphiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1
7 Threads 17
7.1 Un premier exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
7.2 Vie d’un thread et vie de son processus . . . . . . . . . . . . . . . . . . . . . . . . 17
7.3 Commment instancier un thread . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
7.4 Interaction entre threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
7.5 Synchronisation entre threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
7.6 En savoir plus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
8 Documentation 20
Pour des fonctions qui nécessitent plusieurs lignes, on utilisera les accolades et éventuellement
des points-virgules :
var x = 0
def count() = { x += 1; println("Counter: "+x) }
def count() = {
x += 1
println("Counter: "+x)
}
2
On remarque que lorsque la fonction ne prend pas d’argument, on peut omettre les parenthèses
lors de son appel et de sa définition, ce qui pourrait rendre le lecteur du code perplexe :
var x = 0
def count = { x += 1; println("Counter: "+x) }
count
count
Et comme les valeurs et variables peuvent être fonctionnelles le code suivant est valide, nous
reviendrons dessus en Section 4 sur la programmation fonctionnelle.
val plusOne = (n: Int) => n+1
I Écrivez une fonction calculant la factorielle d’un entier de deux manières différentes : avec
une boucle et récursive.
Le sens du mot object sera défini plus tard en Section 3.1. On compile ce fichier en tapant scalac
hello.scala, qui crée des fichiers *.class, puis on lance le programme avec scala MyProgram.
La compilation devient terriblement longue lorsqu’il y a beaucoup de fichiers, pour se simplifier
la tâche nous allons utiliser l’outil sbt.
(build.sbt)
src/
main/
scala/
yourfiles.scala
resources/
(yourimages.png)
3
Vous lancerez la commande sbt à la racine et lorsque vous taperez run ou compile, SBT com-
pilera vos fichiers dans un dossier “target” dont vous n’aurez pas à vous préoccuper. SBT ne
recompile que les sources qui ont été modifiées, ce qui fait gagner beaucoup de temps. Vous met-
trez éventuellement vos fichiers auxiliaires (ex : images) dans le dossier resources. Le fichier
build.sbt contient les options de compilation ; par exemple, pour utiliser la librairie graphique
Swing dont on aura besoin en Section 6, il contiendra :
val swing = "org.scala-lang.modules" %% "scala-swing" % "2.0.1"
2.1 Classes
On regroupe des objets similaires dans une classe. Par exemple, dans une interface graphique,
les fenêtres formeraient une classe. On écrit donc du code en décrivant le comportement des
classes. Pendant l’exécution d’un programme, une classe C peut être instanciée avec le mot-clé
new ; ceci crée un objet de la classe C.
Comme exemple, on regarde un vélo : on considère qu’il dispose d’un compteur kilométrique
et qu’il permet de bouger et freiner.
class Bicycle {
var counter:Double = 0
def move {
counter += 1
println("mon compteur est à"+counter)
}
def brake { println("j’arrête") }
}
4
La première ligne crée une instance de Bicycle ce qui va exécuter le code de la classe en
dehors des def. À remarquer que chaque instance possède son propre compteur. Du coup, on
peut créer deux vélos en même temps, chacun faisant son voyage :
val b = new Bicycle
val c = new Bicycle
while (b.counter < 10) b.move
while (c.counter < 5) c.move
2.2 Héritage
Un aspect intéressant de la programmation orientée objet est le partage de code. Si certains
objets d’une classe C ont des comportements différents ou supplémentaires par rapport aux
autres membres, il convient de les regrouper dans une sous-classe. Les classes forment donc une
hiérachie.
Dans une sous-classe, on ne décrit que les différences à la super-classe. Par exemple, consi-
dérons un vélo de route (qui est plus rapide que les autres) ou un vélo rouillé (qui fait du bruit
lors du freinage) :
class RoadBicycle(name:String) extends Bicycle(name) {
override def move {
counter += 1.5
println(name+": ∗roule∗")
}
}
Le mot-clé override spécifie que la méthode remplace celle de la super-classe. On peut toutefois
réutiliser une fonction remplacée, p.ex. move dans RoadBicycle est équivalent à :
override def move { counter += 0.5 ; super.move }
Une méthode qui accepte comme argument un objet d’une classe C peut travailler sur n’im-
porte quelle sous-classe de C, tout en utilisant les méthodes remplacées de la sous-classe.
I Regroupez les lignes concernant move et brake dans main dans une méthode travel
qui prend comme paramètre un Bicycle et la distance à parcourir. Utilisez-la avec deux vélos
différents.
Toute classe possède automatiquement une méthode toString, mais à priori celle-ci n’est
pas d’une grande utilité :
val b = new Bicycle("Moulinette")
println(b)
5
Pour debugger ses programmes, il peut être très utile d’y inclure plus d’information.
I Redéfinissez toString pour afficher le nom et le compteur d’une bicyclette. Servez-vous
des «chaînes interpolées», p.ex. s"Bonjour $nom" donne Bonjour Jean si nom.toString donne
Jean.
Puisque la classe est abstraite, il est interdit d’en instancier des objets. En plus, toute sous-classe
de Vehicle doit spécifier le comportement concret de move dont on ne connaît que le type.
I Faites de Bicycle une sous-classe de Vehicle et ajoutez une autre sous-classe Scooter.
Modifiez travel pour accepter tout véhicule.
Une classe qui hérite de Runnable contient donc une méthode run (dont le comportement reste
à concrétiser). Avantage des traits : une même classe peut hériter d’une seule super-classe, mais
de multiples traits. Par exemple, les déclarations suivantes sont possibles si B est une classe
(peut-être abstraite) et C, D sont des traits :
class A extends B { ... }
class A extends C { ... }
class A extends B with C { ... }
class A extends B with C with D { ... }
6
ceux qui auraient déjà des notions de programmation orientée objet, les classes « statiques »
n’existent pas en Scala et on se sert des object pour remplir cette fonction.
object A {
var n=0
def incr () { n = n+1 }
def print () { println(n) }
}
A.print()
A.incr()
A.print()
Une autre façon très pratique de créer un objet d’une classe unique est de créer une sous-classe
à la volée au moment de la création de la variable :
class A {
var n=0
def incr () { n=n+1 }
def print () { println(n) }
}
val b = new A {
override def print () { println("value: "+n) }
}
b.incr()
b.print()
val c = Complex(1,2)
val d = c + Complex(-2,3)
println(d)
val e = Complex(1,2)
println(c == e)
Un nombre complexe est défini entièrement par sa partie réélle et imaginaire. Ces données sont
connues lors de la création d’une instance de Complex. Les opérations sur les nombres complexes
ne changent pas leur état interne (contrairement à l’exemple de la bicyclette qui possède son
compteur). Du coup, les case class sont équipées des opérations suivantes :
— On peut fabriquer leurs instances sans new.
— L’opérateur == compare les paramètres de deux instances et non leur identité référentielle.
— Une méthode toString est défini automatiquement (voir le résultat de println).
Du coup, les paramètres d’un case class sont des val immuables, et la pratique de déclarer
des var à l’intérieur est découragée (ceux-ci ne seraient pas pris en compte par ==).
7
I Comparer avec le comportement de == et println quand Complex est une classe ordinaire.
I Programmer une class abstraite Tree avec une méthode sum. Déclarer des sous-classes
Node et Leaf de façon que p.ex. Node(Node(Leaf(2),Leaf(3)),Leaf(5)).sum donne 10.
sum(t)
Lorsque le match est le seul élément de la fonction, on peut écrire :
def sum:Tree=>Int = {
case Leaf(v) => v
case Node(l,r) => sum(l)+sum(r)
}
Dans chaque case d’un pattern matching, on peut ajouter des tests sur les valeurs avec les
syntaxes :
def f:Tree=>Int = {
case Leaf(0) => 0
case Leaf(v) if v < 0 => -1
case Leaf(v) if v > 0 => 1
case Node(l,r) => f(l)+f(r)
}
Et tester l’égalité d’un objet avec des backquotes :
class A {}
val x = new A
val y = new A
def test:A=>Unit = {
case ‘x‘ => println("This is x!")
case ‘y‘ => println("This is y!")
case _ => println("unknown")
}
I Définissez des classes représentant une expression arithmétique sur des entiers relatifs
avec les opérateurs “Add”, “Mul” et “Abs” (valeur absolue). Définissez une fonction qui évalue
l’expression.
I Ajoutez le cas “X” parmi les expressions arithmétiques puis définissez une fonction qui
évalue la dérivée d’une expression par rapport à “X”.
8
4 Collections et Programmation Fonctionnelle
Scala offre de nombreuses classes pour stocker des collections de données. On discutera ces
classes et leurs opérations.
4.1 Tableaux
Commençons avec le type le mieux connu, les Array. Quelques possibilités pour déclarer un
tableau :
val a = Array(1,3,5)
val b = Array[String]("good","bad","ugly")
val c = new Array[Int](3)
Les deux premières lignes créent un Array avec du contenu. Dans le premier cas, le type est
déterminé implicitement par Scala (Int). Dans le deuxième cas, on le spécifie explicitement
(String). Dans le troisième cas, on ne déclare que le type et la taille, le contenu sera la valeur
par défaut du type, dans ce cas 0. Attention, sans le new, c’est une déclaration d’un Array de
Int avec un seul élément, le 3. Deux choses à remarquer.
— Premièrement, Array (et les autres collections) sont un exemple d’une classe polymorphe,
paramétrée par un type (tel que Int, String, ou en fait une classe quelconque).
— Deuxièmement, dans les exemples ci-dessus, a,b,c sont des références à des objets du
type Array[Int] etc. Le fait de déclarer a comme val veut dire que a designera toujours
le même objet pendant sa vie, même si le contenu de cet objet peut muter. Du coup, une
déclaration telle que
var f=a
crée simplement une deuxième référence vers l’objet pointé par a. À remarquer que f est
un var. Du coup,
f(1)=5; f=c; f(2)=5
pour créer un Array de taille 5 dont les éléments sont des Array de Int de taille 3. Accès aux
éléments : p.ex. g(4)(2).
a(2) donne 3, mais a(2)=5 échoue. L’expression 3::a donne une liste avec 3 suivi par les éléments
de a, a:+3 rajoute 3 à la fin, et a++b concatène deux listes. Attention, toutes ces opérations créent
de nouveaux objets de type List, du coup elles auront un coût de O(n), pour n éléments.
9
List est réalisée par des listes chaînées. Du coup, a(i) n’est pas une opération en temps
constant. Pour des accès aléatoires, Vector (parmi d’autres) est plus efficace.
Vous trouverez une liste de différentes collections en Scala sur les pages suivantes :
https://twitter.github.io/scala_school/collections.html
https://twitter.github.io/scala_school/coll2.html.
Les différentes collections se distinguent donc par les opérations possibles sur les objets et leur
efficacité. La page suivante en donne un aperçu :
http://docs.scala-lang.org/overviews/collections/performance-characteristics.html
4.4 Opérations
On est souvent amené à traverser les collections de données. Une possibilité sont les boucles
for. Quelques exemples :
val a = Array(5,2,8,1)
var i = 0;
for (i <- 0 to 3) println(a(i))
for (i <- 0 until a.length) println(a(i))
for (i <- a.indices) println(a(i))
for (i <- a) println(i)
En plus, il existe plusieurs méthodes qui acceptent des fonctions comme paramètre. P.ex.,
foreach accepte une fonction renvoyant un type Unit (qui donc ne renvoie rien) et l’applique à
tous les éléments.
a.foreach(x => println(x))
a.foreach(println(_))
À noter que la seconde forme n’est possible que pour les fonctions anonymes ayant un paramètre
qui n’apparaisse qu’une seule fois.
D’autres opérations permettent de générer de nouvelles listes :
a.map(2 ∗ _)
a.filter(_ >= 5)
10
a.exists(_ >= 7)
a.find(_ >= 7)
a.count(_ <= 7)
a.foldLeft(0)((a,b) => a+b)
Dans la deuxième ligne, le type de retour de find est un type Option[T], une classe abstraite
comme vue en Section 3.3, qui peut soit être None (un case object) soit Some(x:T) où T est le
type des éléments de a. Dans la dernière ligne, 0 est une valeur initiale ; la fonction est d’abord
appliquée sur la valeur initiale et le premier élement, puis sur le résultat et le deuxième élément
et ainsi de suite. Avec reduceLeft, c’est le même principe, mais en omettant la valeur initiale.
Du coup, a.reduceLeft((a,b)=>(a+b)) donne le même résultat dans ce cas. On remarque que
a.reduceLeft(_+_) fonctionne aussi en plus court, et qu’il existe simplement a.sum (ainsi que
a.min, a.max).
I Faites la concaténation des éléments d’un Array[String]. Trouvez aussi la longueur de
la chaîne la plus courte.
Dernière remarque, un tableau multidimensionnel peut être ramené à un tableau simple par
flatten.
Avec le mot-clé yield, une for comprehension peut construire une liste :
val cells = for { x <- 0 to 7; y <- 0 to 7 } yield g(x)(y)
L’opération précédente peut maintenant être exprimée par l’une des lignes suivantes :
for { c <- cells } if (c.x-c.y==3) c.set(1)
for { c <- cells; if c.x-c.y==3 } c.set(1)
cells.filter(c => c.x-c.y==3).foreach(_.set(1))
11
5 Programmation générique
Dans la Section 4 on a vu des classes génériques telles que Array[String] ou List[Int].
Dans ces cas, Array et List sont des classes qui fournissent une fonctionnalité commune pour
des objets de différentes types. Dans cette section, on étudie quelques exemples où ces classes
paramétrées pourront être utiles, et le fonctionnement de base.
5.1 Généricité
Disons qu’on a envie de déclarer une classe de graphes. Dans nos graphes chaque sommet
possède un nom, et il y a un sommet appellé racine :
class Node (val name:String)
class Graph (root:Node) { def getRoot = root }
val n = new Node("racine")
val g = new Graph(n)
println(g.getRoot.name)
Jusqu’ici, tout marche bien. Maintenant, supposons qu’on a envie d’utiliser cette classe dans
de multiples contextes : p.ex. dans un cas, les sommets ne portent que des noms, dans d’autres
ils sont équipés d’un poids, dans encore un autre cas il s’agit d’un arbre généalogique où les
sommets portent des informations sur des personnes (nom, prénom, date de naissance, . . . ). À
défaut d’ajouter toutes les informations réquises dans tous ces contextes à la seule classe Node
(ce qui rendrait cette classe lourde et peu lisible), on préfère la création des sous-classes. À priori,
aucun souci :
class ExtNode (name:String, val weight:Int) extends Node(name)
val n = new ExtNode("racine",5)
val g = new Graph(n)
println(g.getRoot.name)
Graph accepte bien n car ExtNode est une sous-classe de Node. Mais ne serait-il pas intéressant
d’afficher le poids de la racine ? Ajoutons la ligne suivante :
println(g.getRoot.weight)
Ici, Scala se pleint. En fait, le type inféré pour la méthode getRoot est logiquement Node car
root possède ce type. Or, pour obtenir le poids de g.getRoot il faut que Scala sache qu’il
s’agit d’un ExtNode. C’est à ce moment où l’utilité des classes génériques s’avère. On change la
déclaration ainsi :
class Graph[N] (root:N) { def getRoot = root }
Ici, N est un type qui paramètre la classe Graph qui sera spécifié lorsqu’on instancie un graphe.
D’ailleurs, getRoot renvoie alors le type N. Le code suivant marche alors sans problème car le
type de g devient Graph[ExtNode] et g.getRoot devient ExtNode.
val n = new ExtNode("racine",5)
val g = new Graph(n)
println(g.getRoot.weight)
En effet, dans cet exemple, Scale devine que N vaut ExtNode dans new Graph(n) car n possède
ce type. Si on souhaite expliciter ce choix, on peut déclarer new Graph[ExtNode](n).
Parfois, il sera aussi intéressant de créer des méthodes paramétrées :
12
def firstLast[T] (a:List[T]) = List(a.head,a.last)
firstLast(List(1,2,3))
firstLast(List("good","bad","ugly"))
Or, ceci ne fonctionne plus car root est désormais du type N, et Scala ne connaît rien sur ce
type ; en particulier Scala est incapable d’inférer que ce type possède un membre name. Or, si on
revenait sur l’ancienne définition de class Graph (root:Node), on serait incapable d’évaluer
g.getRoot.weight. La solution consiste à spécifier que N doit être un sous-classe de Node.
class Graph[N <: Node] (root:N)
Inversement, >: spécifie une contrainte de super-classe. Regardons le code suivant (il convient
d’utiliser l’interpréteur scala) :
class A
class B extends A { def mergeWith[T](x:T) = List(x,this) }
val a = new A ; val b = new B
List(a,b)
b.mergeWith(a)
On constate que le type inféré de List(a,b) est List[A] mais que b.mergeWith(a) donne
List[Any], bien que ces deux listes sont composées des mêmes éléments : lorsque Scala compile
la méthode mergeWith, la super-classe la plus profonde entre T et B n’est que Any. La modification
suivante permet de sauver la mise :
class B extends A { def mergeWith[T >: B](x:T) = List(x,this) }
On exige alors que T soit une superclasse de B ce qui permet à Scala d’inférer le type List[T]
pour mergeWith. D’ailleurs, cette contrainte n’empêche pas de passer des sous-classes de B, le
résultat de mergeWith sera toutefois du type List[B].
class C extends B
val c = new C
b.mergeWith(c)
c.mergeWith(c)
13
class ExtNode (name:String, val weight:Int) extends Node(name)
class Graph[N <: Node] (nodes:List[N], edges:List[Edge])
{ def outgoing (n:N) = edges.filter(_.from == n)
def successors (n:N) = outgoing(n).map(_.to) }
val n1 = new ExtNode("n1",5)
val n2 = new ExtNode("n2",3)
val edge = new Edge(n1,n2)
val g = new Graph(List(n1,n2),List(edge))
g.successors(n1).foreach(n => println(n.name))
Cet exemple fonctionne, mais échoue si on remplace name par weight dans la dernière ligne ;
logiquement car successors travaille avec une liste de Edge qui ne relie que des Node. Il convient
alors de paramétrer la classe Edge elle aussi :
class Edge[N] (val from:N, val to:N)
class Graph[N <: Node] (nodes:List[N], edges:List[Edge[N]])
...
g.successors(n1).foreach(n => println(n.weight))
Supposons maintenant qu’on veut utiliser la classe Graph dans un contexte où les arêtes portent
des étiquettes, et qu’on a envie de récupérer les étiquettes sur les arêtes sortantes de n1. On se
définit alors une sous-classe d’arêtes :
class ExtEdge[N] (from:N, val label:String, to:N) extends Edge(from,to)
val n1 = new ExtNode("n1",5)
val n2 = new ExtNode("n2",3)
val edge = new ExtEdge(n1,"a",n2)
val g = new Graph(List(n1,n2),List(edge))
g.outgoing(n1).foreach(e => println(e.label))
Mais bien sûr cela échoue car outgoing renvoie toujours List[Edge]. La solution consiste alors
à donner deux paramètres interdépendants à la classe Graph :
class Graph[N <: Node, E <: Edge[N]] (nodes:List[N], edges:List[E])
6 Interfaces Graphiques
Dans cette section nous allons voir comment créer des interfaces graphiques simples avec la
librairie Swing.
14
}
On utilise la méthode add d’un tel conteneur, qui prend en argument un Component et des
contraintes de position. Ici Component est une classe abstraite qui pourrait être en particulier :
— un Label pour afficher du texte ou une image ;
— un Button pour interagir avec l’utilisateur ;
— un autre conteneur comme BorderPanel, GridPanel ou tout autre sous-classe de Panel ;
— un TextField ou TextArea comme formulaire de texte ;
— etc.
I Essayez ces différents composants à l’aide de la documentation en ligne (accessible à cette
adresse : http://scala-lang.org/documentation/api.html) pour voir ce qu’ils permettent.
15
title = "MyApplication"
contents = myButton
listenTo(myButton)
reactions += {
case ButtonClicked(source) => println("Thanks")
case _ => {}
}
}
}
Ici le pattern matching concerne des ComponentEvent tels que ButtonClicked ou EditDone, qui
prennent en argument le composant source de l’action.
I Créez à l’aide de ces outils une mini calculatrice qui affiche le produit de nombres entrés
dans des TextField lorsqu’on appuie sur un bouton.
I Créez un convertisseur Celsius/Fahrenheit avec deux champs textes mais sans bouton, qui
s’actualise dès que l’utilisateur change l’une des deux valeurs.
I Créez une grille de 20 par 10 boutons, qui affichent leur coordonnées lorsque l’on clique
dessus.
16
icon = new ImageIcon(getClass.getResource("myimage.png"))
I Modifiez la grille précédente pour que les boutons affichent une image lorsqu’on clique
dessus.
7 Threads
Scala fournit une interface simplifiée pour travailler avec des threads. Un thread est une tâche
qui s’exécute en parallèle du reste du programme.
I Exécutez l’exemple plusieurs fois pour observer différents entrelacements entre les deux
threads.
17
— En C, le fait de terminer la fonction main entraîne un appel implicite d’exit. Ceci n’est
pas le cas pour Scala – le compilateur assure que le processus reste en vie tant qu’il existe
au moins un thread et fait exit seulement après.
I Créez une extension Bavardeur de Runnable qui émet 10 fois une même ligne dont le
contenu est un paramètre. Refaites l’exemple précédent en utilisant cette classe.
18
override def run {
try {
...
} catch {
case e:InterruptedException => ...
}
}
Si une interruption intervient lorsque le thread exécute le bloc try, l’exécution de ce bloc
sera terminée et le contrôle transféré vers la partie catch. La partie après la flèche (=>) peut
être vide.
I Modifiez la classe Bavardeur de telle sorte qu’elle attende une seconde entre deux lignes.
Faites en sorte que la méthode main interrompe l’un des threads après 3 secondes ; le thread
devrait réagir en disant « ouf » et en terminant.
I Exécutez le programme ci-dessus. Vous observerez que la valeur atteinte par i est bien
loin de la valeur attendue (200000).
Scala permet une façon simplifiée d’utiliser des sémaphores pour gérer les accès aux données
partagées. Tout objet est implicitement associé à un sémaphore. On considère le code suivant
où o est un objet quelconque :
o.synchronized { ... }
Ce code obtient d’abord le sémaphore associé à o, puis exécute le code entre accolades et ensuite
relâche le sémaphore. L’obtention du sémaphore n’est possible que pour un seul thread à la fois ;
19
si jamais deux threads essayent de l’obtenir, l’un des deux doit attendre jusqu’à ce que le premier
relâche le sémaphore.
I Modifiez le programme ci-dessus pour que i atteigne toujours la valeur 200000.
8 Documentation
Lorsque l’on collabore à plusieurs sur un projet (ou pour tout projet de grande taille, même
effectué seul), il est essentiel que chaque participant documente les divers éléments de code qu’il
produit (classes, méthodes, etc.) afin que les autres (ou même lui-même) puissent les utiliser
comme des « boîtes noires », sans devoir directement lire le code en question pour savoir comment
ils fonctionnent.
Scaladoc (fourni avec Scala) est un système générant de la documentation (sous forme de
pages HTML) à partir de commentaires spécifiques que le programmeur ajoute aux fichiers
sources. Devant chaque élément (classe, méthode, attribut, etc.) que l’on souhaite documenter,
on place un bloc de commentaire débutant par /**, terminant par */ et contenant générale-
ment un texte descriptif court, un texte descriptif plus long (qui peut néanmoins être omis) et
d’autres informations introduites par des étiquettes, comme par exemple @param pour ajouter
un descriptif lié à un paramètre d’un constructeur ou d’une méthode.
Voici un court exemple :
/∗∗
∗ Descriptif court de la classe ‘C‘.
∗
∗ Descriptif long de la classe ‘C‘.
∗
∗ Celui-ci peut s’étaler sur plusieurs lignes et paragraphes.
∗
∗ @author Informations concernant l’auteur.
∗
∗ @constructor
∗
∗ Descriptif court du constructeur primaire de la classe ‘C‘.
∗
∗ @param p Descriptif du paramètre de ce constructeur.
∗/
class C(p:Int)
{
/∗∗
∗ Descriptif court de l’attribut ‘a1‘.
∗
∗ Descriptif long de l’attribut ‘a1‘.
∗/
20
var a1 = 0
/∗∗ Descriptif court de l’attribut ‘a2‘. ∗/
val a2 = Array(293762, 89072398, 9872723, 2389723)
/∗∗
∗ Descriptif court de la méthode ‘m1‘.
∗
∗ Descriptif long de la méthode ‘m1‘.
∗
∗ @param p Descriptif du paramètre ‘p‘ de la méthode ‘m1‘.
∗ @return Descriptif de ce qui est retourné par la méthode ‘m1‘.
∗/
def m1 (p:Int):Int = p
/∗∗
∗ Descriptif court de la méthode ‘m2‘.
∗/
def m2 = {}
}
Pour générer la documentation pour un ensemble de fichiers sources, il suffit alors de lancer
la commande scaladoc sur ces fichiers, ce qui aura pour effet de produire une arborescence de
pages de documentation en HTML affichables à l’aide d’un simple navigateur web (la racine de
l’arborescence correspondant au fichier index.html).
Scaladoc propose de nombreuses autres étiquettes pour ajouter une multitude d’informations
de types divers et supporte également le « markup » pour formatter le texte. Plus d’informations
à ce sujet et une documentation plus complète se trouvent à cette adresse : http://docs.
scala-lang.org/overviews/scaladoc/for-library-authors.html.
21