Tutoriel pour apprendre le langage Scala par l'exemple


précédentsommairesuivant

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 :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

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