R avancé et introduction à Git

Fonctions, Objets S3

Pourquoi écrire des fonctions ?

Exemple

Exemple de code : une data.frame contient 4 colonnes de données de températures en degrés C°. On souhaite convertir les degrés Celsius en Farheneit selon la formule \(F° = C° * 9 / 5 + 32\).

df <- data.frame(
    temp_a = rnorm(20, 2),
    temp_b = rnorm(20, 2),
    temp_c = rnorm(20, 2),
    temp_d = rnorm(20, 2)
)

df$a <- (df$a * 9 / 5) + 32
df$b <- (df$b * 9 / 5) + 32
df$c <- (df$c * 9 / 5) + 32
df$d <- (df$a * 9 / 5) + 32

Motivation à l’écriture d’une fonction

  • Eviter les copier/coller, et les erreurs associées, factoriser le code et réutiliser du code existant
  • Faciliter la maintenance et les évolutions du code en un seul endroit
  • Faciliter la lecture et la compréhension du code en donnant un nom évocatif

Syntaxe

faire_une_fonction <- function(w, x, y = 1, z = NULL, ...) {
    # commentaire pour les développeurs
    if (!is.null(z)) {
        return(z)
    }
    w + x + y # retourne le résultat de la dernière expression évaluée
}

fonction_sur_une_ligne <- function(x) x + 1
  • Les arguments peuvent prendre des valeurs par défaut ;
  • on peut écrire une fonction sur une ligne sans {} si elle est suffisament simple ;
  • lorsque ce n’est pas trivial, il faut expliciter l’instruction de retour avec la fonction return().

Convertir en fonction

Ici, combien d’input(s) doit-on définir en entrée de la fonction ?

# conversion celsius/farheneit
df$a <- (df$a * 9 / 5) + 32

un seul input doit être défini, correspondant à la variable df$a.

Nommage des fonctions : les fonctions traduisent des actions. Il faut donc utiliser un verbe pour traduire cette action dans le nom. On privilégiera le snake_case.

Convertir en fonction

Quelle devrait être la nature de l’objet x ? Vecteur ou data.frame ?

convert_celsius_to_farheneit <- function(x) {}

Convertir en fonction

Quelle devrait être la nature de l’objet x ? Vecteur ou data.frame ?

convert_celsius_to_farheneit <- function(x) {
    x_farheneit <- (x * 9 / 5) + 32
    return(x_farheneit)
}

Il est important de définir les paramètres comme étant les objets minimaux nécessaires à la fonction, donc ici un vecteur plutôt qu’une data.frame.

Principes de développement

Il faut essayer de suivre certains principes de design de code :

  • Limiter le nombre d’arguments à ses fonctions (4 ? …)
  • Utiliser des noms explicites, et des verbes
  • Définir des fonctions pures dans la philosophie de la programmation fonctionnelle.
  • Utiliser des objets : S3, S4, R6 …

Packaging

Nous verrons comment tester, documenter et packager ses fonctions par la suite.

Définition d’une fonction

Les fonctions

Les fonctions sont des objets.

f <- function(x, y) {
    x + y
}

Les fonctions

Les fonctions sont des objets

f <- function(x, y) {
    x + y
}

body(f) # défini explicitement, code de la fonction
{
    x + y
}
formals(f) # défini explicitement, ses arguments
$x


$y
environment(f) # basé sur où est définie la fonction
<environment: R_GlobalEnv>

3 types de fonctions

typeof(mean)
typeof(sum)
typeof(`[`)

3 types de fonctions

typeof(mean)
typeof(sum)
typeof(`[`)
[1] "closure"
[1] "builtin"
[1] "special"
  • La plupart des fonctions sont de type closure
  • Sauf des fonctions spéciales maintenues et écrites par R-core, builtin et special

Scope d’une fonction

x <- 4
y <- 5
f00 <- function(x) {
    return(x + 1)
}

f00(1)
f00(x)
f00(y)

Scope d’une fonction

x <- 4
y <- 5
f00 <- function(x) {
    return(x + 1)
}

f00(1)
[1] 2
f00(x)
[1] 5
f00(y)
[1] 6

Scope d’une fonction

y <- 5
f01 <- function() {
    return(y + 1)
}

print("premier appel", f01())
y <- 10
print("second appel", f01())

Scope d’une fonction

y <- 5
f01 <- function() {
    return(y + 1)
}

f01()
y <- 10
f01()
[1] 6
[1] 11

La valeur de y est définie au moment de l’exécution, et non au moment de la définition de f01().

Scope d’une fonction

x <- 3
y <- 5
f02 <- function() {
    x <- 2
    f_in <- function() {
        x + y
    }
    print(environment(f_in))
    f_in()
}

print(environment(f02))
f02()
x <- 5
f02()

Scope d’une fonction

x <- 3
y <- 5
f02 <- function() {
    x <- 2
    f_in <- function() {
        x + y
    }
    print(environment(f_in))
    f_in()
}

print(environment(f02))
f02()
x <- 5
f02()
<environment: R_GlobalEnv>
<environment: 0x5564349706d0>
[1] 7
<environment: 0x5564349cfc28>
[1] 7

Scope d’une fonction

  • Si un nom n’est pas défini dans l’environnement d’une fonction, alors on regarde “un niveau au-dessus”, dans l’environnement parent.
  • Un nom défini dans l’environnement de la fonction “masque” la valeur d’un objet de même nom défini dans un environnement parent.
  • R cherche la valeur quand la fonction est exécutée, non quand la fonction est définie.
  • R fait la différence entre les fonctions et les variables.

Quelques conseils

  • Ne pas nommer ses variables du même nom qu’une fonction
  • Faire des fonctions “pures” et définir les variables utiles dans les paramètres de la fonction

Arguments

f01 <- function(a, b) a + b**2
f01(1, 2)
f01(b = 1, 2)
  • les paramètres non-nommés sont associés à l’argument positionnellement
f01(1, 2)
[1] 5
  • il est possible de mélanger les paramètres nommés et non-nommés. Attention à l’ordre de ceux-ci !
f01(b = 1, 2)
[1] 3

Matching partiel

Matching partiel

En R, les arguments sont identifiés partiellement !

f02 <- function(x, xyz, xylophone) x + xylophone**2
f02(1, 2, xylo = 3)
f02(1, 2, xy = 3)

Matching partiel

Matching partiel

En R, les arguments sont identifiés partiellement !

f02 <- function(x, xyz, xylophone) x + xylophone**2
f02(1, 2, xylo = 3)
[1] 10
f02(1, 2, xy = 3)
Error in `f02()`:
! argument 3 matches multiple formal arguments

Note

  • Utilisez des arguments nommés si possible
  • Essayez de respecter l’ordre des arguments
  • Privilégiez le nom exact de l’argument

… (dot-dot-dot)

La notation ... permet de propager des arguments à une autre fonction. Dans d’autres langages de programmation, on retrouve ce concept de varargs soit arguments variables. Ceci permet à la fonction de prendre un nombre variable d’arguments.

f <- function(...) list(...)
f(a = 2, b = "hello")
f(x = c(1, 2, 3))

… (dot-dot-dot)

La notation ... permet de propager des arguments à une autre fonction. Dans d’autres langages de programmation, on retrouve ce concept de varargs soit arguments variables. Ceci permet à la fonction de prendre un nombre variable d’arguments.

f <- function(...) list(...)
f(a = 2, b = "hello")
$a
[1] 2

$b
[1] "hello"
f(x = c(1, 2, 3))
$x
[1] 1 2 3

… (dot-dot-dot)

Note

Utile pour passer des arguments supplémentaires, en particulier quand un des arguments de la fonction est lui même une fonction, par exemple pour lapply

Quelques points de vigilance

  • peut laisser passer des ‘typos’ sans lever d’erreurs ;
  • rend plus difficile la compréhension de ce que fait la fonction et nécessite une documentation précise.
sapply(list(c(1:10), c(1:10, NA)), mean, na.rm = TRUE)
[1] 5.5 5.5
sapply(list(c(1:10), c(1:10, NA)), mean, na_rm = TRUE)
[1] 5.5  NA

Output d’une fonction

convert_celsius_to_farheneit <- function(x) {
    x_farheneit <- (x * 9 / 5) + 32
    return(x_farheneit)
}

Similaire à :

convert_celsius_to_farheneit <- function(x) {
    x_farheneit <- (x * 9 / 5) + 32
    x_farheneit
}

Une fonction retourne l’objet contenu dans l’appel explicite à return() ou le résultat de la dernière expression évaluée de manière implicite.

Contrôle d’arguments

C’est une bonne pratique de vérifier des conditions importantes sur les arguments.

f04 <- function(x, y) {
    x + y
}

Contrôle d’arguments

C’est une bonne pratique de vérifier des conditions importantes sur les arguments.

f04 <- function(x, y) {
    if (!is.numeric(x)) stop("x not numeric")
    if (!is.numeric(y)) stop("y not numeric")
    x + y
}
f04(1, 2)
f04(1, "a")

Contrôle d’arguments

C’est une bonne pratique de vérifier des conditions importantes sur les arguments.

f04 <- function(x, y) {
    if (!is.numeric(x)) stop("x not numeric")
    if (!is.numeric(y)) stop("y not numeric")
    x + y
}
f04(1, 2)
[1] 3
f04(1, "a")
Error in `f04()`:
! y not numeric

Contrôle d’arguments

C’est une bonne pratique de vérifier des conditions importantes sur les arguments.

f04 <- function(x, y) {
    stopifnot(is.numeric(x), is.numeric(y))
    x + y
}
f04(1, 2)
f04(1, "a")

Contrôle d’arguments

C’est une bonne pratique de vérifier des conditions importantes sur les arguments.

f04 <- function(x, y) {
    stopifnot(is.numeric(x), is.numeric(y))
    x + y
}
f04(1, 2)
[1] 3
f04(1, "a")
Error in `f04()`:
! is.numeric(y) is not TRUE

Erreurs, avertissements, messages

Trois types de signaux : errors, warning, message. En R, il est possible d’attraper les erreurs ou les messages.

?tryCatch
?try
?withCallingHandlers

Pour aller plus loin

Voir le chapitre sur les conditions dans Advanced-R https://adv-r.hadley.nz/conditions.html#conditions

Opérateurs et fonctions spéciales

Quelques fonctions ont un comportement spécial : elles s’écrivent entre les arguments. Exemples, les opérateurs mathématiques :

1 + 2
3 * 4

On aurait pu écrire :

`+`(1, 2)
`*`(3, 4)

Liste des fonctions de ce type : :, ::, :::, $, @, ^, *, /, +, -, >, >=, <, <=, ==, !=, !, &, &&, |, ||, ~, <-, ->, <<-., etc.

Opérateurs et fonctions spéciales

Définir ses propres opérateurs :

`%+%` <- function(lhs, rhs) {
    paste0(lhs, rhs)
}

"hell" %+% "o"
[1] "hello"

Autres fonctions spéciales :

  • (, {, [, [[, next, repeat, break, if, for, while, repeat, function

Opérateurs et fonctions spéciales : pipe et composition de fonctions

Opérateurs et fonctions spéciales : pipe et composition de fonctions

Composition de fonctions :

square <- function(x) x^2
deviation <- function(x) x - mean(x)
x <- runif(100)
# Population standard deviation
sqrt(mean(square(deviation(x))))
[1] 0.2890192

Avec des pipes magrittr :

library(magrittr)

x %>%
    deviation() %>%
    square() %>%
    mean() %>%
    sqrt()
[1] 0.2890192

Avec R base (>4.1) :

x |>
    deviation() |>
    square() |>
    mean() |>
    sqrt()
[1] 0.2890192

Fonctions anonymes

Parfois, il n’est pas nécessaire de choisir d’associer un nom à une fonction, et on peut utiliser des fonctions anonymes.

x <- 1:3
sapply(x, function(x) x**2 - 1)
[1] 0 3 8
integrate(function(x) 1 / ((x + 1) * sqrt(x)), lower = 0, upper = Inf)
3.141593 with absolute error < 2.7e-05
liste <- list(
    f = function(x) x**2,
    g = \(x) x - 1
) # syntaxe possible depuis R > 4.1
liste$f(10)
[1] 100
liste$g(2)
[1] 1

Objets S3

Exemple : Date

date_cours <- as.Date("2023-01-13", format = "%Y-%m-%d")
str(date_cours)
 Date[1:1], format: "2023-01-13"
typeof(date_cours)
[1] "double"
class(date_cours)
[1] "Date"
attributes(date_cours)
$class
[1] "Date"

Les objets de type Date sont de classe Date, même si leur représentation interne est double1.

Attribut de classe

Un objet S3 est défini par son attribut class.

x <- 1:9
str(x)
 int [1:9] 1 2 3 4 5 6 7 8 9
class(x) <- "ma_premiere_classe"
str(x)
 'ma_premiere_classe' int [1:9] 1 2 3 4 5 6 7 8 9
class(x)
[1] "ma_premiere_classe"

Attribut de classe

R est très permissif.

mod <- lm(log(mpg) ~ log(disp), data = mtcars)
class(mod)
[1] "lm"
class(mod) <- "Date"
print(mod)
Error in `as.POSIXlt()`:
! 'list' object cannot be coerced to type 'double'

S3

R n’a aucun moyen d’assurer que tous les objets d’une classe aient la même structure. Il faut aider l’utilisateur en définissant :

  • un constructeur
  • un validateur
  • un wrapper user-friendly autour du constructeur si l’objet a vocation à être exposé.

Voir le chapitre 13.3 d’Advanced R pour plus de détails et d’informations sur la philosophie des objets S3.

Fonctions et méthodes génériques

Une classe S3 permet le dispatch de fonctions génériques.

# définir une fonction générique :
nouvelle_fonction_generique <- function(x) {
    UseMethod("nouvelle_fonction_generique")
}

Fonctions génériques courantes : print, summary, plot

  • Facilite l’apprentissage des APIs des librairies pour les utilisateurs
  • Le dispatch permet de gérer l’héritage de classe. R applique la bonne méthode au bon objet.

Exemple

new_integer <- function(x) {
    stopifnot(is.integer(x))
    structure(x, class = "new_integer") # altern. à `class<-`
}

print.new_integer <- function(x, ...) {
    cat("new_integer object\n")
    cat("min_value: ", min(x), " - max_value: ", max(x), "\n")
}

x <- new_integer(1:40L)
print(x)
new_integer object
min_value:  1  - max_value:  40 
print(unclass(x)) # le même vecteur sans attribut de classe
 [1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
[26] 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

Autres paradigmes objets en R

R n’est pas connu pour être un langage de programmation objet. Et pourtant, outre S3, il existe d’autres paradigmes : S4, RC, R6, S7

  • Paradigmes parfois différents des paradigmes courants en Java, Python, etc ;
  • S3 doit être le choix par défaut; sa simplicité et flexibilité répond à de nombreux besoins.

Questions ?