[How-to] Partager le contenu entre plusieurs instances R6

Le système orienté objet est une approche simple et puissante permettant de programmer avec R. Pourtant, ekke peut être un peu intimidante au début, surtout si vous avez toujours codé en R, avec des fonctions. Dans ce billet, nous allons nous pencher du côté de {R6}, l’un des packages les plus téléchargés du CRAN, et l’un des piliers d’un grand nombre de packages modernes. Et sourtout, nous allons répondre spécifiquement à la question du partage de données entre les instances d’une classe.

C’est quoi {R6}?

En quelques mots, {R6} est un cadre moderne et flexible orienté objet (« OO ») pour R.

Alors oui, votre prochaine question pourrait être : que signifie « orienté objet » ? Avec l’OO, vous créez des objets, et à l’intérieur de ces objets se trouvent des données et des méthodes (c’est-à-dire des fonctions). L’exécution de ces méthodes peut modifier les données contenues dans l’objet.

Comme dans tout OO, {R6} possède un système de classes et d’instances de ces classes – ce qui amène au sujet de ce post : comment partager du contenu entre toutes les instances d’une classe ?

Quand utiliser {R6}?

Un cas d’utilisation est celui où vous avez besoin de construire un objet, et que vous voulez encapsuler à l’intérieur de cet objet des données, et des méthodes pour effectuer des actions sur ces données.  On pourrait bien sûr faire cela dans l’environnement global, en créant des objets et des fonctions.  Mais l’idée ici est d’avoir quelque chose qui est empaqueté à l’intérieur du même objet, et que tous les objets de la même classe commencent avec le même contenu et les mêmes méthodes.

R6 peut être utilisé pour gérer la connexion à la base de données, par exemple. A l’intérieur de l’objet R6, vous trouverez toutes les informations sur la connexion, ainsi que les méthodes spécifiquement liées à l’interaction avec la base de données. Vous pouvez également construire des documents, à l’intérieur de l’objet.

Voici quelques exemples de packages utilisant une API orientée R6 :

Jouons avec {R6}

Nous allons créer une classe R6 pour générer du code HTML et CSS, afin d’avoir une page web.

Disclaimer : bien sûr, cette classe R6 est (vraiment) limitée pour la génération de pages web, l’idée ici est de fournir un exemple, pas d’écrire un générateur de site web prêt à l’emploi 😉

library(R6)
library(glue)
library(purrr)
library(magrittr)
library(htmltools)


WebPage <- R6Class("WebPage", 
                   public = list(
                     name = character(0),
                     head = c("<!DOCTYPE html>","<html>","<head>"),
                     body = "<body>",
                     style = '<style type="text/css">',
                     add_style = function(identifier, content){
                       content <- imap_chr(content, ~ glue("{.y} : {.x};")) %>%
                         unname() %>% 
                         paste(collapse = " ") 
                       glued <- glue("%identifier% { %content% }", 
                                     .open = "%", .close = "%")
                       self$style <- c(self$style, glued)
                     },
                     initialize = function(name){
                       self$name <- name
                     },
                     add_tag = function(tag, content){
                       glued <- glue("<{tag}>{content}</{tag}>")
                       self$body <- c(self$body, glued)
                     },
                     save = function(path){
                       write(private$concat(self$head, self$style, self$body), 
                             glue("{file.path(path, self$name)}.html"))
                     },
                     view = function(){
                       html_print(HTML(private$concat(self$head, self$style, self$body)))
                     },
                     print = function(){
                       cat(private$concat(self$head, self$style, self$body), sep = "\n")
                     }
                   ), 
                   private = list(
                     concat = function(head, style, body){
                       c(head, style, "</style>", body,"</body>","</html>")
                     }
                   )
)

a <- WebPage$new("index")

a$add_style("body", list("font-family" = "Helvetica", "color" = "#24292e"))
a$add_style("h2", list("font-size" = "3 em", "color" = "#911414", 
                       "text-align" = "center"))
a$add_style("h3", list("font-size" = "1.5 em", "color" = "#2E2E8F", 
                       "text-align" = "center"))


a$add_tag("h2", "Hey there!")
a$add_tag("h3", "You've reached the rtask teams!")
a$add_tag("p", "We are glad to have you here.")
a$add_tag("p", "Enjoying R6 already?")
a
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
body { font-family : Helvetica; color : #24292e; }
h2 { font-size : 3 em; color : #911414; text-align : center; }
h3 { font-size : 1.5 em; color : #2E2E8F; text-align : center; }
</style>
<body>
<h2>Hey there!</h2>
<h3>You've reached the rtask teams!</h3>
<p>We are glad to have you here.</p>
<p>Enjoying R6 already?</p>
</body>
</html>
a$view()


a$save(".")

Ok, découpons tout ce qu’on a fait en morceaux :

WebPage <- R6Class("WebPage", 

Ici, nous donnons un nom à notre classe. Par convention, on utilise le CamelCase.

                   public = list(
                     name = character(0),
                     head = c("<!DOCTYPE html>","<html>","<head>"),
                     body = "<body>",
                     style = '<style type="text/css">',

Avec ces lignes de code, je suis en train de créer une liste publique, qui est en quelques mots la liste des éléments qui sont accessibles à l’utilisateur une fois que l’objet est créé.

                     initialize = function(name){
                       self$name <- name
                     },

`initialize` est la fonction qui est appelée lorsque vous faites class$new(arg =). Ici, nous avons choisi de donner à `self$name` un nom, qui sera utilisé plus tard avec la méthode de sauvegarde.

                     add_style = function(identifier, content){
                       content <- imap_chr(content, ~ glue("{.y} : {.x};")) %>%
                         unname() %>% 
                         paste(collapse = " ") 
                       glued <- glue("%identifier% { %content% }", 
                                     .open = "%", .close = "%")
                       self$style <- c(self$style, glued)
                     },

Cette fonction est utilisée pour ajouter un élément CSS. Par exemple, quand je fais :

a$add_style("h3", list("font-size" = "1.5 em", 
                       "color" = "#2E2E8F", 
                       "text-align" = "center"))

Je veux que la sortie soit :

h3 {font-size : 1.5 em; 
    color = #2E2E8F; 
    text-align = center;}

C’est ce que je fais ici : sur la liste nommée donnée en paramètre, j’`imap` une fonction personnalisée qui créera, pour chaque élément de la liste, un élément id : param ;.

Ensuite, on le met dans { contenu }, et comme j’ai besoin d’utiliser `{` comme caractère réel, j’utilise % pour fournir un caractère d’ouverture et de fermeture (comme `{glue}` utilise `{` pour marquer l’élément à d’unquoting par défaut, et celui-ci peut être remplacé).

                     add_tag = function(tag, content){
                       glued <- glue("<{tag}>{content}</{tag}>")
                       self$body <- c(self$body, glued)
                     },

Facile ici, je crée des balises html 😉

                      save = function(path){
                       write(private$concat(self$head, 
                                                      self$style, 
                                                      self$body), 
                             glue("{file.path(path, self$name)}.html")
                             )
                     },

Ici, je crée une fonction pour enregistrer la page html, donc au final il s’agit juste une écriture, avec un chemin d’accès.

                     view = function(){
                       html_print(HTML(private$concat(self$head, 
                                                      self$style, 
                                                      self$body)))
                     },

This one is used to view the content of the page inside your default browser. I made the concat a private method (it concatenates the elements), HTML turns characters into HTML, and html_print shows the content.

                     print = function(){
                       cat(private$concat(self$head, self$style, self$body), sep = "\n")
                     }
                   ), 

Il existe une méthode `print` par défaut pour les objets R6, mais elle peut être remplacée. Il suffit juste de spécifier ce qui se passe lorsque l’objet défini est imprimé dans la console.

                   private = list(
                     concat = function(head, style, body){
                       c( head, style, "</style>", body, "</body>","</html>")
                     }
                   )
)

Et enfin, notre liste privée. J’ai choisi de mettre `concat` ici car je ne voulais pas et n’avais pas besoin que cette fonction soit accessible en dehors de l’objet. Mais j’aurais aussi pu la mettre en dehors de la classe.

Plusieurs pages, un CSS

Il serait préférable que toutes les instances de la classe partagent le même CSS, n’est-ce pas ?

Mais voici le problème : une fois qu’un objet est instancié, son contenu public est scellé (par rapport à sa famille) : si A et B sont des instances de la classe C, changer quelque chose dans la liste publique de A ne le changera pas en B. Et si vous changez quelque chose en C, cela ne « descend » pas aux instances de la classe. Ce qui est un comportement auquel on peut s’attendre : si je change le titre de A, je ne veux pas que le titre de B soit changé.

Mais dans notre cas, nous devons pouvoir accéder aux mêmes données à partir de plusieurs instances : en d’autres termes, je veux que toutes mes instances de WebPages accèdent au même code CSS. Pour ce faire, nous utiliserons des environnements R. Dans le champ privé de notre définition de classe R6, nous allons mettre en place un environnement « partagé ». Ensuite, à chaque fois que je créerai un nouvel objet de cette classe, la variable partagée pointera vers le même environnement (et donc in fine, vers le même contenu CSS).

Faisons cela :

WebSkeleton <- R6Class("WebSkeleton", 
                       public = list(
                         name = character(0),
                         head = c("<!DOCTYPE html>","<html>","<head>"),
                         body = "<body>",
                         add_style = function(identifier, content){
                           content <- imap_chr(content, ~ glue("{.y} : {.x};")) %>%
                             unname() %>% 
                             paste(collapse = " ") 
                           glued <- glue("%identifier% { %content% }", 
                                         .open = "%", .close = "%")
                           private$shared$style <- c( private$shared$style, glued)
                         },
                         add_tag = function(tag, content){
                           glued <- glue("<{tag}>{content}</{tag}>")
                           self$body <- c(self$body, glued)
                         },
                         initialize = function(name){
                           self$name <- name
                         },
                         save = function(path){
                           write(private$concat(self$head, self$style, self$body), 
                                 glue("{file.path(path, self$name)}.html")
                           )
                         },
                         view = function(){
                           html_print(HTML(private$concat(self$head, private$shared$style, self$body)))
                         },
                         print = function(){
                           cat(private$concat(self$head, private$shared$style, self$body), 
                               sep = "\n")
                         }
                       ), 
                       private = list(
                         concat = function(head, style, body){
                           c(head, style, "</style>", body,"</body>","</html>")
                         }, 
                         shared = {
                           env <- new.env()
                           env$style <- '<style type="text/css">'
                           env
                         }
                       )
)

# Créons une nouvelle page
index <- WebSkeleton$new("index")

index$add_style("body", list("font-family" = "Helvetica", 
                         "color" = "#24292e"))
index$add_style("h2", list("font-size" = "3 em", "color" = "#911414", 
                       "text-align" = "center"))
index$add_style("h3", list("font-size" = "1.5 em", "color" = "#2E2E8F", 
                       "text-align" = "center"))

index$add_tag("h2", "Hey there!")
index$add_tag("h3", "You've reached the rtask teams!")
index$add_tag("p", "We are glad to have you here.")
index$add_tag("p", "Enjoying R6 already?")
index
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
body { font-family : Helvetica; color : #24292e; }
h2 { font-size : 3 em; color : #911414; text-align : center; }
h3 { font-size : 1.5 em; color : #2E2E8F; text-align : center; }
</style>
<body>
<h2>Hey there!</h2>
<h3>You've reached the rtask teams!</h3>
<p>We are glad to have you here.</p>
<p>Enjoying R6 already?</p>
</body>
</html>
index$view()


# Et une autre page
team <- WebSkeleton$new("team")
# Sans assigner aucun style
team$add_tag("h2", "You again?")
team$add_tag("h3", "Time to meet the team!")
team$add_tag("p", "Vincent: [email protected] - https://twitter.com/VincentGuyader")
team$add_tag("p", "Diane: [email protected] - https://twitter.com/DianeBELDAME")
team$add_tag("p", "Colin: [email protected] - https://twitter.com/_ColinFay")
team$add_tag("p", "Sebastien: [email protected] - https://twitter.com/StatnMap")

team$view()

Comme vous pouvez le voir, je n’ai pas eu à assigner de style pour avoir accès au CSS. Et si j’ajoute un autre style à mon objet Team :

team$add_style("p", list("font-weight" = "bold"))
team$view()


Cela sera partagé avec la page index :

index$view()

🎉

Le CSS est maintenant partagé entre toutes les instances de la classe « WebSkeleton ».

Comment ça marche?

Voici donc une démonstration rapide pour vous montrer que les éléments publics ne sont pas partagés entre les instances de classe, mais que les environnements le sont.

plop <- R6Class("plop", 
                public = list(a = 1, 
                              change_a = function(val) { self$a <- val }, 
                              print_b = function() { private$shared$b }, 
                              change_b = function(val) { private$shared$b <- val} ), 
                private = list( shared = { e <- new.env(); e$b <- 2; e } )
)

plop_1 <- plop$new()
plop_1$a
[1] 1

plop_2 <-  plop$new()
plop_2$change_a(12)
plop_2$a
[1] 12

plop_1$a
[1] 1

plop_1$print_b()
[1] 12
plop_2$print_b()
[1] 12

plop_1$change_b(19)
plop_1$print_b()
[1] 19
plop_2$print_b()
[1] 19

Ici, une fois que plop_1 est initialisé, il contient la variable a avec une valeur spécifique. Si j’initie plop_2 et change_a, la seule valeur qui est changée est celle qui se trouve dans plop_2.

Mais lorsqu’il s’agit de b, puisque l’environnement partagé est un environnement – et cet environnement est copié par référence et non par valeur –, nous pouvons partager les données entre toutes les instances.

Conceptuellement parlant, chaque instance de la variable a pointe vers son propre espace en mémoire, alors que tous les éléments partagés pointent vers le même endroit, donc contiennent la même valeur.

 


À propos de l'auteur

Colin Fay

Colin Fay

Data scientist & R Hacker



Also read