R avancé et introduction à Git

Raphaël Nedellec

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.

Convertir du code en fonction

Ctrl+Alt+X sous Windows, Control+Option+X sous Mac.

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 lors du cours n° 3

Définition d’une fonction

Les fonctions

Les fonctions sont des objets.

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

Les fonctions

Les fonctions sont des objets

f <- function(x, y) {
  # commentaire
  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: 0x1117b6320>
#> [1] 7
#> <environment: 0x1130794d8>
#> [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(1, 2, xy = 3): 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(1, "a"): 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(1, "a"): 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

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.2775706

Avec des pipes, magrittr ou R base (>4.1) :

x |> # R base (>4.1)
  deviation() |>
  square() |>
  mean() |>
  sqrt()
#> [1] 0.2775706
x %>% # maggritr
  deviation() %>%
  square() %>% 
  mean() %>%
  sqrt()
#> [1] 0.2775706

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

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

Un objet S3 est défini par son attribut class. 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(.Internal(Date2POSIXlt(x, tz)), tz = tz): '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")
  r <- range(x, ...)
  cat("min_value: ", r[[1L]], " - max_value: ", r[[2L]], "\n")
  cat(head(x), "......", tail(x), "\n")
  invisible(x)
}

x <- new_integer(1:40L)
print(x)
#> new_integer object
#> min_value:  1  - max_value:  40 
#> 1 2 3 4 5 6 ...... 35 36 37 38 39 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

  • 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 ?