Obtenga todas las funciones de origen

11

En R, estoy usando source()para cargar algunas funciones:

source("functions.R")

¿Es posible obtener la lista de todas las funciones definidas en este archivo? Como nombres de funciones. (¿Tal vez source()sí mismo de alguna manera puede devolverlo?).

PD: El último recurso sería llamar por source()segunda vez como local({ source(); })y luego hacer ls()funciones internas y filtrar, pero eso es demasiado complicado: ¿hay una solución más fácil y menos torpe?

TMS
fuente
1
Esto no sirve source(), pero este viejo hilo puede ser de su interés.
Andrew
1
@ Andrew, gracias, he comprobado las soluciones propuestas, pero eso suena mucho más loco que el último recurso que presenté en la pregunta :)
TMS
2
No lo sé, esta solución es más loca:envir <- new.env() source("functions.R", local=envir) lsf.str(envir)
LocoGris
2
Crea un paquete con tus archivos fuente. Entonces obtienes todas las ventajas, incluido un espacio de nombres de paquete.
Roland
@TMS, entendió mal su pregunta / no leyó que quería funciones definidas . Disculpas!
Andrew

Respuestas:

7

Creo que la mejor manera sería obtener el archivo en un entorno temporal. Consulte ese entorno para todas las funciones, luego copie esos valores en el entorno principal.

my_source <- function(..., local=NULL) {
  tmp <- new.env(parent=parent.frame())
  source(..., local = tmp)
  funs <- names(tmp)[unlist(eapply(tmp, is.function))]
  for(x in names(tmp)) {
    assign(x, tmp[[x]], envir = parent.frame())
  }
  list(functions=funs)
}

my_source("script.R")
MrFlick
fuente
gracias, esta solución parece prometedora, ¡como la única en este momento! Sorprendentemente, el que tiene menos votos a favor. Es el que mencioné como último recurso, pero en new.env()lugar del elegante, local({ })no estoy seguro de si funcionaría con el assignmarco principal.
TMS
1) ¿crees que funcionaría local()? Y por cierto, 2) lo que haces en el bucle for: ¿no hay alguna función para fusionar entornos?
TMS
1
@TMS Puede funcionar con local, aunque no lo he probado. No conozco otra forma de copiar todas las variables de un entorno a otro. No es una operación común.
MrFlick
Creo que attachse puede utilizar para, bueno, unir un entorno a otro. Aunque debe usar el posargumento en lugar de especificar el parent.frame. Y solo funcionará bien para copiar todo el entorno, el forbucle de MrFlick le permite copiar solo las funciones.
Gregor Thomas el
5

Es un poco torpe, pero podría ver los cambios en los objetos antes y después de la sourcellamada de esta manera.

    # optionally delete all variables
    #rm(list=ls())

    before <- ls()
    cat("f1 <- function(){}\nf2 <- function(){}\n", file = 'define_function.R')
    # defines these
    #f1 <- function() {}
    #f2 <- function() {}
    source('define_function.R')
    after <- ls()

    changed <- setdiff(after, before)
    changed_objects <- mget(changed, inherits = T)
    changed_function <- do.call(rbind, lapply(changed_objects, is.function))
    new_functions <- changed[changed_function]

    new_functions
    # [1] "f1" "f2"
Andrew Chisholm
fuente
¡Gracias! También tuve esta idea, pero no funciona por una razón muy simple: si el paquete ya estaba cargado (lo que sucede todo el tiempo cuando depuro el código, solo vuelvo a buscar las fuentes), entonces no devuelve nada.
TMS
3

Creo que esta expresión regular captura casi todos los tipos válidos de funciones (operador binario, funciones de asignación) y todos los caracteres válidos en el nombre de una función, pero es posible que haya perdido un caso límite.

# lines <- readLines("functions.R")

lines <- c(
  "`%in%` <- function",
  "foo <- function",
  "foo2bar <- function",
  "`%in%`<-function",
  "foo<-function",
  ".foo <-function",
  "foo2bar<-function",
  "`foo2bar<-`<-function",
  "`foo3bar<-`=function",
  "`foo4bar<-` = function",
  "` d d` <- function", 
  "lapply(x, function)"
)
grep("^`?%?[.a-zA-Z][._a-zA-Z0-9 ]+%?(<-`)?`?\\s*(<-|=)\\s*function", lines)
#>  [1]  1  2  3  4  5  6  7  8  9 10
funs <- grep("^`?%?[.a-zA-Z][._a-zA-Z0-9 ]+%?(<-`)?`?\\s*(<-|=)\\s*function", lines, value = TRUE)
gsub("^(`?%?[.a-zA-Z][._a-zA-Z0-9 ]+%?(<-`)?`?).*", "\\1", funs)
#>  [1] "`%in%`"      "foo "        "foo2bar "    "`%in%`"      "foo"        
#>  [6] ".foo "       "foo2bar"     "`foo2bar<-`" "`foo3bar<-`" "`foo4bar<-`"
alan ocallaghan
fuente
1
digo, creo que esta no es realmente una buena solución, pero definitivamente es una solución divertida . Probablemente convertiría el archivo a un paquete si realmente necesitara esta información.
alan ocallaghan
¡Me he perdido dos casos extremos! Las funciones pueden comenzar con .y funciones de asignación ( `foo<-`<- function(x, value)existen.
alan ocallaghan
Lo uso =para asignación, esto no captará ninguna de mis funciones ...
Gregor Thomas
Buena captura - editado. Notaré que R te permite hacer cosas tontas como las ` d d` <- function(x)que actualmente no están atrapadas. No quiero que la expresión regular se vuelva demasiado tonta, aunque podría volver a visitarla.
alan ocallaghan
Además, puede asignar funciones con assign, <<-y ->. Y será muy difícil hacer que este enfoque tenga en cuenta las funciones que se definen dentro de las funciones, pero que en realidad no se encuentran en el entorno de origen. Su respuesta debería funcionar muy bien para los casos estándar, pero en realidad no desea escribir un analizador R a partir de expresiones regulares.
Gregor Thomas el
1

Si este es su propio script para que tenga control sobre cómo está formateado, una simple convención sería suficiente. Solo asegúrese de que cada nombre de función comience en el primer carácter de su línea y que la palabra functiontambién aparezca en esa línea. Cualquier otro uso de la palabra functiondebe aparecer en una línea que comienza con un espacio o tabulación. Entonces una solución de una línea es:

sub(" .*", "", grep("^\\S.*function", readLines("myscript.R"), value = TRUE))

Las ventajas de este enfoque son que

  • Es muy simple . Las reglas simplemente se establecen y solo se necesita una línea simple de código R para extraer los nombres de las funciones. Regex también es simple y para un archivo existente es muy fácil de verificar, solo haga clic en la palabra functiony verifique si cada aparición que se muestra sigue la regla.

  • No es necesario ejecutar la fuente. Es completamente estático .

  • en muchos casos no necesitará cambiar el archivo fuente en absoluto y en otros habrá cambios mínimos. Si está escribiendo el guión desde cero con esto en mente, es aún más fácil de organizar.

Hay muchas otras alternativas a lo largo de la idea de convenciones. podría tener una expresión regular más sofisticada o podría agregar# FUNCTION al final de la primera línea de cualquier definición de función si está escribiendo el script desde cero y luego extrae esa frase y extrae la primera palabra en la línea, pero la sugerencia principal aquí parece particularmente atractivo debido a su simplicidad y las otras ventajas enumeradas.

Prueba

# generate test file
cat("f <- function(x) x\nf(23)\n", file = "myscript.R") 

sub(" .*", "", grep("^\\S.*function", readLines("myscript.R"), value = TRUE))
## [1] "f"
G. Grothendieck
fuente
lapply(x, function(y) dostuff(y))rompería esto
alan ocallaghan
@alan ocallaghan, su ejemplo viola las reglas establecidas, por lo que no puede ocurrir de manera válida. Para escribir esto y seguir dentro de las reglas, uno tendría que comenzar a funcionar en una nueva línea con sangría o podría tener que sangrar el lapply.
G. Grothendieck
Creo que la utilidad se degrada enormemente si usted requiere un formato específico, ya que ello podría requerir cambiar el archivo - en cuyo caso, usted puede también sugerir al usuario leer los nombres de las funciones de forma manual
Alan O'Callaghan
1
Eso es solo una consideración si no controlas el archivo pero hemos excluido esa posibilidad. Usar convenciones es muy común en la programación. A menudo pongo # TODOtodo mi código para poder hacer mis tareas, por ejemplo. Otra posibilidad en la misma línea sería escribir # FUNCTIONal final de la primera línea de cualquier definición de función.
G. Grothendieck
1
tratar de analizar con expresiones regulares es el camino al infierno ...
TMS
0

Esto adapta el código utilizado en la publicación de mi comentario para buscar una secuencia de tokens (símbolo, operador de asignación, luego función), y debe tomar cualquier función definida. No estoy seguro de si es robusto como la respuesta de MrFlick, pero es otra opción:

source2 <- function(file, ...) {
  source(file, ...)
  t_t <- subset(getParseData(parse(file)), terminal == TRUE)
  subset(t_t, token == "SYMBOL" & 
           grepl("ASSIGN", c(tail(token, -1), NA), fixed = TRUE) & 
           c(tail(token, -2), NA, NA) == "FUNCTION")[["text"]]
}
Andrés
fuente