44 votes

Comment créer une fonction R de manière programmatique ?

Hadley Wickham a récemment posé une question intéressante sur le mailing r-devel et comme je n'ai pas trouvé de question sur le sujet sur StackOverflow, j'ai pensé qu'il serait utile qu'elle existe également ici.

Pour paraphraser :

Une fonction R est constituée de trois éléments : une liste d'arguments, un corps et un environnement. Peut-on construire une fonction de manière programmatique à partir de ces trois éléments ?

(Une réponse assez complète se trouve à la fin du fil de discussion dans le lien r-devel ci-dessus. Je laisse le champ libre à d'autres personnes pour qu'elles recréent elles-mêmes le benchmarking des différentes solutions et le fournissent comme réponse, mais veillez à citer Hadley si vous le faites. Si personne ne se manifeste d'ici quelques heures, je le ferai moi-même).

1 votes

Je pense que c'est pour cela qu'ils ont inventé les lisps !

0 votes

Aussi étrange que cela puisse paraître, nous avons vu un cas où l'on pourrait vouloir analyser une chaîne de caractères pour réaliser quelque chose de similaire (knitr).

52voto

joran Points 68079

Ceci est un développement de la discussion aquí .

Nos trois pièces doivent être une liste d'arguments, un corps et un environnement.

Pour l'environnement, nous utiliserons simplement env = parent.frame() par défaut.

Nous ne voulons pas vraiment d'une bonne vieille liste pour les arguments, donc à la place nous utilisons alist qui a un comportement différent :

"...les valeurs ne sont pas évaluées, et les arguments balisés sans valeur sont autorisés"

args <- alist(a = 1, b = 2)

Pour le corps, nous quote notre expression pour obtenir un call :

body <- quote(a + b)

Une option consiste à convertir args à une liste de paires et ensuite simplement appeler la fonction function en utilisant eval :

make_function1 <- function(args, body, env = parent.frame()) {
      args <- as.pairlist(args)
      eval(call("function", args, body), env)
}

Une autre option consiste à créer une fonction vide, puis à la remplir avec les valeurs souhaitées :

make_function2 <- function(args, body, env = parent.frame()) {
      f <- function() {}
      formals(f) <- args
      body(f) <- body
      environment(f) <- env

      f
}

Une troisième option consiste à utiliser simplement as.function :

make_function3 <- function(args, body, env = parent.frame()) {
      as.function(c(args, body), env)
}

Et enfin, cela me semble très similaire à la première méthode, sauf que nous utilisons un idiome quelque peu différent pour créer l'appel de fonction, en utilisant substitute plutôt que call :

make_function4 <- function(args, body, env = parent.frame()) {
      subs <- list(args = as.pairlist(args), body = body)
      eval(substitute(`function`(args, body), subs), env)
}

library(microbenchmark)
microbenchmark(
      make_function1(args, body),
      make_function2(args, body),
      make_function3(args, body),
      make_function4(args, body),
      function(a = 1, b = 2) a + b
    )

Unit: nanoseconds
                          expr   min      lq  median      uq    max
1 function(a = 1, b = 2) a + b   187   273.5   309.0   363.0    673
2   make_function1(args, body)  4123  4729.5  5236.0  5864.0  13449
3   make_function2(args, body) 50695 52296.0 53423.0 54782.5 147062
4   make_function3(args, body)  8427  8992.0  9618.5  9957.0  14857
5   make_function4(args, body)  5339  6089.5  6867.5  7301.5  55137

0 votes

Joran, excellent résumé, merci. Pourriez-vous commenter le benchmarking s'il vous plaît ? D'après votre résultat, il semble que la fonction 1 soit légèrement plus rapide que les autres fonctions. Mais avec l'alternative idiote : ` function(a = 1, b = 2, c = 3, d = 4, e = 5, f = 6, g = 7, h = 8, i = 9, j = 10) { exp(a + b) * (c + d)^e / f - ln(g) + h^i^j }` j'obtiens que la fonction4 semble beaucoup plus rapide que les autres. Une idée ?

0 votes

Extension de make_function1 dans votre code pour ne jamais écraser les fonctions existantes, j'ai trouvé ce qui suit : github.com/tidyverse/lubridate/issues/

8voto

Moody_Mudskipper Points 18115

rlang a une fonction appelée new_function qui fait cela :

Utilisation

nouvelle_fonction(args, body, env = caller_env())

library(rlang)
g <- new_function(alist(x = ), quote(x + 3))
g
# function (x) 
# x + 3

7voto

yoplait Points 113

Il y a aussi la question de la création alist de manière programmatique, car cela peut être utile pour créer des fonctions lorsque le nombre d'arguments est variable.

Un alist est simplement une liste nommée de symboles vides. Ces symboles vides peuvent être créés avec substitute() . Donc :

make_alist <- function(args) {
  res <- replicate(length(args), substitute())
  names(res) <- args
  res
}

identical(make_alist(letters[1:2]), alist(a=, b=))
## [1] TRUE

0voto

Thevandalyst Points 61

Je ne suis pas sûr que cela vous aidera, mais le code ci-dessous pourrait être utile dans certains scénarios,

bonjour_monde peut être la chaîne de caractères qui sera utilisée pour créer la fonction et qui sera utilisée pour nommer la fonction. bonjour_monde

hello_world <- "print('Hello World')"

assign("Hello",function()
  {
    eval(parse(text = hello_world))
  }, envir = .GlobalEnv)

Cela va créer une fonction appelée hello_world

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X