Cours 4 — Programmation orientée objet

Patrick Fournier

MAT8186 — Techniques avancées en programmation statistiques R

Automne 2023

Introduction

OOP 101

  • Paradigme sous lequel le concept d'objet joue un rôle central
  • Objet physique
    table, chargé de cours
    Concept
    matrice, paix dans le monde
  • Approche OO
    1. Définir un ensemble d'objets
    2. Définir les interactions entre ces objets

OOP et langage

  • Plus un état d'esprit qu'une caractéristique d'un langage
  • Plus facile dans certains langages
  • Premier langage OO: Simula 67

Terminologie 1

Classe
Déclaration (parfois définition) de la structure interne d'un ensemble d'objets
Slot
Variable associée à un objet / une classe (aussi field, attribut)
Méthode
Fonction associée à un ou plusieurs objet(s) / classe(s)

Terminologie 2

Héritage
Processus par lequel une classe fille acquiert la structure d'une ou plusieurs classes mères
Polymorphisme
Une unique interface pour plusieurs entités de type différents
Encapsulation
Cacher l'implémentation à l'utilisateur

Terminologie 3

Message passing
Communication entre les objets
  • Messages \( = \) idée fondamentale de l'OOP (Alan Kay & Smalltalk)
  • Souvent limité à des appels de méthodes (C++, Java...)
  • Concept beaucoup plus large (Smalltalk, Groovy...)

Terminologie 4

Fonction générique
Sert à dispatcher les appels de méthodes
  • Choisit la méthode répondant le mieux a notre besoin
  • Origines: Flavors (Lisp Machine Lisp) & CommonLoops (Common Lisp)
  • Approche «de base» de R

OOP et R

Systèmes d'objets

  • S3
    • Fonctions génériques
    • Pas de classes formelles
  • S4
    • S3 \( + \) classes formelles
  • Reference classes (RC, R5)
    • Message passing
    • Instances mutables
  • R6
    • «Version améliorée» de RC

Caractéristiques des systèmes

  • S3
    • Très informel
    • Peu de fonctionnalités
    • Facile à apprendre
    • Facile à utiliser
    • Souvent suffisant
  • S4
    • Moins performant que S3
    • Plus difficile à utiliser
  • RC
    • Moins performant que R6
    • Objets mutables
    • basé sur S4
  • R6
    • Basé sur S3
    • Essentiellement RC, mais «mieux»

Choisir un système

  • Par défaut: S3
  • Si la mutabilité est importante: R6
  • S4 si vous avez besoin
    • Héritage multiple (plus d'une classe mère)
    • Dispatch multiple (dispatch sur plusieurs arguments)
    • Intégration avec Bioconductor
  • RC si vous êtes obligés 😉

S3

Attribut class

  • S3 est basé sur une seule chose: l'attribut class
  • De type character
  • Entrées ordonnées en ordre décroissant de spécificité

Structure

  • Objet S3 composé de
    • Un objet «standard»
    • Un attribut class
  • Objet standard: souvent une list, mais pas d'obligation

                    r$> p1 <- c(1, 2)
                    r$> class(p1) <- c("point", "numeric")
                

                    r$> p2 <- c(1, 2)
                    r$> attributes(p2) <- list(class = c("point",
                                                         "numeric"))
                

                    r$> p3 <- structure(c(3, 4), class = c("point",
                                                           "numeric"))
                

Constructeur

  • L'appel à class<-, attributes<- ou structure n'est ni élégant, pratique ou sécuritaire
  • Il est d'usage de définir un (plusieurs) constructeur(s) 👷
  • Simple fonction qui se charge de l'appel
  • Permet verifications
  • Convention: nom constructeur \( = \) nom classe

                    point <-  function(v){
                        v <- as.numeric(v)

                        stopifnot(identical(length(v), 2L))

                        structure(v, class = c("point", class(v)))
                    }
                

                    r$> point(1:3)
                    Error in point(1:3): identical(length(v), 2L)
                        is not TRUE
                

                    r$> point(letters[1:2])
                    Warning message in point(letters[1:2]):
                    “NAs introduced by coercion”
                    [1] NA NA
                    attr(,"class")
                    [1] "point"   "numeric"
                

                    point(1:2)
                    [1] 1 2
                    attr(,"class")
                    [1] "point"   "numeric"
                

Méthodes

  • Reconaissables par leur nom: méthode.classe(...)
  • Facile de définir de nouvelles méthodes
  • Possible d'implémenter des méthodes pour une classe que l'on a pas définie: type piracy 🏴‍☠️

Méthode print

  • r$> var \( \Leftrightarrow \) r$> print(var)
  • En ce moment, appel dispatché à une méthode plus générale


Nous allons:
  1. Définir print pour point
  2. Définir norme retournant la norme euclidienne du vecteur correspondant

                    print.point <- function(x, ...)
                        cat("x =", x[1],
                        "& y =", x[2], "\n")


                

                    r$> p1 <- 1:2
                    x = 1 & y = 2
                

                    norme.point <- function(x)
                        as.numeric(sqrt(crossprod(x)))
                

                    r$> norme(point(c(3, 4)))
                    Error in norme(point(c(3, 4))) : could
                    not find function "norme"
                
Confused

Method dispatch

  • Deux appels différents à une même fonction peuvent donner un résultat différent
  • Différence: classe de l'argument
  • Polymorphisme

                    r$> summary(cars)
                         speed           dist
                     Min.   : 4.0   Min.   :  2.00
                     1st Qu.:12.0   1st Qu.: 26.00
                     Median :15.0   Median : 36.00
                     ...
                

                    r$> summary(lm(dist ~ speed, cars))
                    Call:
                    lm(formula = dist ~ speed, data = cars)

                    Residuals:
                        Min      1Q  Median      3Q     Max
                    -29.069  -9.525  -2.272   9.215  43.201

                    Coefficients:
                                Estimate Std. Error t value Pr(> |t|)
                    ...
                

Exemple: summary

  • summary ne calcule rien, n'affiche rien
  • Détermine la méthode appropriée à appeller: method dispatch
  • Fonction qui dispatch: fonction générique
  • En R: appellent UseMethod
  • Trouve la méthode la plus spécialisée pouvant s'appliquer

                    r$> methods(summary)
                     [1] summary.aov                    summary.aovlist*
                     [3] summary.aspell*                summary.check_packages_in_dir*
                     [5] summary.connection             summary.data.frame
                     [7] summary.Date                   summary.default
                     [9] summary.ecdf*                  summary.factor
                    [11] summary.glm                    summary.infl*
                    [13] summary.lm                     summary.loess*
                    [15] summary.manova                 summary.matrix
                    [17] summary.mlm*                   summary.nls*
                    [19] summary.packageStatus*         summary.POSIXct
                    [21] summary.POSIXlt                summary.ppr*
                    [23] summary.prcomp*                summary.princomp*
                    [25] summary.proc_time              summary.srcfile
                    [27] summary.srcref                 summary.stepfun
                    [29] summary.stl*                   summary.table
                    [31] summary.tukeysmooth*           summary.warnings
                
Exemple

                    v1 <- structure(c(1, 2),
                                    class = c("vec", "numeric"))
                

                    r$> summary.vec(v1)
                    Error in summary.vec(v1) : could not find
                      function "summary.vec"
                

                    r$> summary.numeric(v1)
                    Error in summary.vec(v1) : could not find
                      function "summary.numeric"
                

                    r$> summary(v1)
                       Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
                       1.00    1.25    1.50    1.50    1.75    2.00
                

Méthode par défaut

Exemple précédent: summary (la générique) cherchait méthode pour
  1. vec 😟
  2. numeric 😟

Plus de classe, mais summary fonctionne quand même...
Méthode par défaut!

                    r$> summary.default(v1)
                       Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
                       1.00    1.25    1.50    1.50    1.75    2.00
                

Fonction générique

  • Méthode sans fonction générique: 😟
  • Parfois nécessaire de définir nos propres génériques
  • 99% du temps: uniquement UseMethod, 2 arguments
    1. generic: nom de la méthode à appeler
    2. object: objet utilisé pour le dispatch (premier argument passé à la générique par défaut) 🪄

                    norme <- function(x, ...)
                        UseMethod("norme")
                

                    r$> norme(point(c(3, 4)))
                    [1] 5
                
Exemple: Déterminer si des points sont alignés.

                    isaligned.point <- function(...) {
                        points <- list(...)

                        isTRUE(length(points) <= 2) && return(TRUE)

                        fit <- crossprod(
                            solve(cbind(1, c(points[[1]][1], points[[2]][1]))),
                            c(points[[1]][2], points[[2]][2]))

                        for (p in points[-(1:2)]) {
                            pred <- crossprod(c(1, p[1]), fit)[1]

                            isTRUE(all.equal(pred, p[2])) || return(FALSE)
                        }
                        TRUE
                    }
                

                    r$> isaligned.point(point(1:2),
                                        point(3:4),
                                        point(5:6))
                    [1] TRUE
                

                    r$> isaligned.point(point(1:2),
                                        point(3:4),
                                        point(5:7))
                    [1] FALSE
                
Avec une générique!

                    isaligned <-  function(...)
                        UseMethod("isaligned")
                

                    r$> isaligned(point(1:2),
                                  point(5:6),
                                  point(9:10))
                    [1] TRUE
                

                    r$> isaligned(point(1:2),
                                  point(5:6),
                                  point(9:11))
                    [1] FALSE
                

Point marqué

  • Classe point: simple spécialisation de numeric
  • Peut se complexifier: héritage
  • Exemple: point marqué
    • Hérite de point
    • Ajoute un attribut
    • Autre possibilié: utiliser une liste
      1. point
      2. marque

                    pointM <- function(v, mark = NULL) {
                        stopifnot(is.null(mark) ||
                                     (is.character(mark) &&
                                      identical(length(mark), 1L)))

                        p <- point(v)
                        attr(p, "mark") <- mark

                        structure(p, class = c("pointM", class(p)))
                    }
                

                    r$> p1 <- pointM(1:2, "Premier point")
                    r$> p2 <- pointM(3:4, "Second point")
                    r$> p3 <- pointM(5:6, "Troisième point")
                

                    r$> p1
                    x = 1 & y = 2

                    r$> norme(p2)
                    [1] 5

                    r$> isaligned(p1, p2, p3)
                    [1] TRUE
                

Retour sur pointM

  • Héritage conceptuellement correct: tout ce qui peut être fait avec un point devrait pouvoir se faire avec un point marqué
  • Pas nécessairement le contraire
  • Définissons la méthode mark

                    mark.pointM <- function(point)
                        print(attr(point, "mark"))

                    mark <- function(point)
                        UseMethod("mark")
                

                    r$> mark(p1)
                    [1] "Premier point"
                

Setter

Impossible de modifier la marque de la manière habituelle

                    r$> mark(p1) <- "1er point"
                    Error in mark(p1) <- "1er point":
                      could not find function "mark<-"
                

                    `mark<-.pointM` <- function(p, v) {
                        attr(p, "mark") <- v
                        p
                    }

                    `mark<-` <- function(p, v, ...)
                        UseMethod("mark<-")
                

                    r$> mark(p1) <- "1er point"

                    r$> mark(p1)
                    [1] "1er point"
                

Performance

  • Système par fonction générique: grande flexibilité
  • Coût: plus d'appels de fonctions
  • En général, pas un problème
  • Dans code critique (boucle...), considérer appeler directement la méthode