14. Valeurs paresseuses▲
Les valeurs paresseuses (ou valeurs à la demande) permettent de différer l'évaluation d'une valeur jusqu'à ce qu'elle soit utilisée pour la première fois. Ceci peut être notamment utile lorsque l'on manipule des valeurs qui pourraient ne pas être nécessaires au calcul et dont le coût du traitement est significatif. Prenons l'exemple d'une base de données d'employés où chaque employé contient son supérieur hiérarchique et les membres de son équipe :
2.
3.
4.
case
class
Employee
(
id: Int
, name: String
, managerId: Int
) {
val
manager: Employee =
Db.get
(
managerId)
val
team: List
[Employee]
=
Db.team
(
id)
}
Cette classe Employee initialisera immédiatement tous ses champs en chargeant toute la table des employés en mémoire. Ceci n'est sûrement pas optimal et peut être aisément amélioré en utilisant des champs paresseux (lazy) : l'accès à la base de données sera ainsi différé jusqu'au moment où elle est réellement nécessaire.
2.
3.
4.
case
class
Employee
(
id: Int
, name: String
, managerId: Int
) {
lazy
val
manager: Employee =
Db.get
(
managerId)
lazy
val
team: List
[Employee]
=
Db.team
(
id)
}
Pour voir ce qui se passe réellement, nous pouvons nous servir de cette base d'exemple qui affiche quand ses enregistrements sont lus :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
object
Db {
val
table =
Map
(
1
->
(
1
, "Haruki Murakami"
, -
1
),
2
->
(
2
, "Milan Kundera"
, 1
),
3
->
(
3
, "Jeffrey Eugenides"
, 1
),
4
->
(
4
, "Mario Vargas Llosa"
, 1
),
5
->
(
5
, "Julian Barnes"
, 2
))
def
team
(
id: Int
) =
{
for
(
rec <-
table.values.toList; if
rec._3 ==
id)
yield
recToEmployee
(
rec)
}
def
get
(
id: Int
) =
recToEmployee
(
table
(
id))
private
def
recToEmployee
(
rec: (
Int
, String
, Int
)) =
{
println
(
"[db] fetching "
+
rec._1) Employee
(
rec._1, rec._2, rec._3)
}
}
Lorsque l'on exécute un programme qui récupère un seul employé, l'affichage confirme que la base de données n'est accédée que lorsque l'on accède aux variables paresseuses.
Un autre cas d'utilisation de ces variables est la résolution de l'ordre d'initialisation des applications composées de plusieurs modules : avant que nous ne connaissions l'existence des variables paresseuses, nous utilisions pour cela des définitions d'objets. Considérons, par exemple, un compilateur composé de plusieurs modules. Examinons d'abord une table des symboles qui définit une classe pour les symboles et deux fonctions prédéfinies :
2.
3.
4.
5.
6.
7.
8.
9.
10.
class
Symbols
(
val
compiler: Compiler) {
import
compiler.types._
val
Add =
new
Symbol
(
"+"
, FunType
(
List
(
IntType, IntType), IntType))
val
Sub =
new
Symbol
(
"-"
, FunType
(
List
(
IntType, IntType), IntType))
class
Symbol
(
name: String
, tpe: Type) {
override
def
toString =
name +
": "
+
tpe
}
}
Le module symbols est paramétré par une instance de Compiler qui fournit l'accès à d'autres services, comme le module types. Dans notre exemple, il n'existe que deux fonctions prédéfinies, l'addition et la soustraction, dont les définitions dépendent du module types.
2.
3.
4.
5.
6.
7.
class
Types
(
val
compiler: Compiler) {
import
compiler.symtab._
abstract
class
Type
case
class
FunType
(
args: List
[Type]
, res: Type) extends
Type
case
class
NamedType
(
sym: Symbol
) extends
Type
case
object
IntType extends
Type
}
Pour lier ces deux composants, on crée un objet compilateur et on leur passe en paramètre :
2.
3.
4.
class
Compiler {
val
symtab =
new
Symbols
(
this
)
val
types =
new
Types
(
this
)
}
Malheureusement, cette approche simple échoue à l'exécution, car le module symtab a besoin du module types. En règle générale, la dépendance entre les modules peut être compliquée et il est difficile d'obtenir le bon ordre d'initialisation - voire impossible lorsqu'il y a des cycles. Une solution simple à ce problème consiste à rendre ces champs paresseux et à laisser le compilateur s'occuper de l'ordre :
2.
3.
4.
class
Compiler {
lazy
val
symtab =
new
Symbols
(
this
)
lazy
val
types =
new
Types
(
this
)
}
Les deux modules seront désormais initialisés lors de leur premier accès et le compilateur peut s'exécuter comme on l'avait prévu.
Syntaxe
Le modificateur lazy n'est autorisé qu'avec les définitions de valeurs concrètes. Toutes les règles de typage s'appliquent également aux valeurs paresseuses et les valeurs locales récursives sont également autorisées.