Obter todas as funções de origem

11

No R, estou usando source()para carregar algumas funções:

source("functions.R")

É possível obter a lista de todas as funções definidas neste arquivo? Como nomes de funções. (Talvez source()ele próprio possa devolvê-lo de alguma forma?).

PS: O último recurso seria chamar a source()segunda vez como local({ source(); })e depois ls()executar funções internas e de filtro, mas isso é muito complicado - existe solução mais fácil e menos desajeitada?

TMS
fonte
11
Isso não usa source(), mas esse segmento antigo pode ser do seu interesse.
18719 Andrew Andrew
11
@ Andrew graças, eu verifiquei as soluções propostas, mas que sons maneira mais louca do que o último recurso I apresentado na pergunta :)
TMS
2
Não sei se essa solução é mais louca:envir <- new.env() source("functions.R", local=envir) lsf.str(envir)
LocoGris 15/11/19
2
Faça um pacote com seus arquivos de origem. Então você obtém todas as vantagens, incluindo um espaço para nome do pacote.
Roland
@TMS, não entendeu a sua pergunta / não leu que você queria funções definidas . Desculpas!
18719 Andrew Andrew

Respostas:

7

Eu acho que a melhor maneira seria originar o arquivo em um ambiente temporário. Consulte esse ambiente para todas as funções e copie esses valores para o ambiente pai.

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
fonte
obrigado, esta solução parece promissora, como a única agora! Surpreendentemente, aquele com menos votos positivos. É o que eu mencionei como último recurso, mas usando em new.env()vez do elegante local({ })que não tenho certeza se funcionaria com o assignquadro pai.
TMS
1) você acha que funcionaria local()? E BTW, 2) o que você faz no loop for: não há alguma função para mesclar ambientes?
TMS
11
@TMS Pode funcionar com local, embora eu não tenha tentado. Não conheço outra maneira de copiar todas as variáveis ​​de um ambiente para outro. Não é uma operação comum.
MrFlick
Eu acho que attachpode ser usado para, bem, conectar um ambiente a outro. Embora você precise usar o posargumento em vez de especificar o parent.frame. E isso só funcionará bem para copiar todo o ambiente, o forloop do MrFlick permite copiar apenas as funções.
Gregor Thomas
5

É um pouco desajeitado, mas você pode observar alterações nos objetos antes e depois da sourcechamada dessa maneira.

    # 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
fonte
Obrigado! Eu também tive essa ideia, mas ela não funciona por uma razão muito simples - se o pacote já estiver carregado (o que acontece o tempo todo quando depuro o código, eu apenas recoloquei as fontes), ele não retorna nada.
TMS
3

Eu acho que esse regex captura quase todos os tipos de funções válidas (operador binário, funções de atribuição) e todos os caracteres válidos em um nome de função, mas eu posso ter perdido um caso extremo.

# 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
fonte
11
Eu acho que essa não é realmente uma boa solução, mas é definitivamente uma solução divertida . Provavelmente converteria o arquivo em um pacote se realmente precisasse dessas informações.
alan Ocallaghan
Perdi dois casos extremos! As funções podem começar com .e funções de atribuição ( `foo<-`<- function(x, value)exist.
alan Ocallaghan
Eu uso =para atribuição, isso não vai pegar nenhuma das minhas funções ...
Gregor Thomas
Boa captura - editada. Observarei que R permite que você faça coisas tolas como as ` d d` <- function(x)que atualmente não são capturadas. Eu não quero que o regex fique muito bobo, embora eu possa revisitar.
alan Ocallaghan
Além disso, você pode atribuir funções com assign, <<-e ->. E será muito difícil tornar essa abordagem responsável pelas funções definidas dentro das funções, mas que não estão realmente no ambiente de origem. Sua resposta deve funcionar muito bem para casos padrão, mas você realmente não deseja escrever um analisador R a partir de regex.
Gregor Thomas
1

Se esse for seu próprio script, para que você tenha controle sobre como ele é formatado, uma simples convenção seria suficiente. Apenas verifique se o nome de cada função começa no primeiro caractere de sua linha e se a palavra functiontambém aparece nessa linha. Qualquer outro uso da palavra functiondeve aparecer em uma linha que começa com um espaço ou tabulação. Em seguida, uma solução de uma linha é:

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

As vantagens dessa abordagem são que

  • é muito simples . As regras são simplesmente declaradas e há apenas uma linha simples de código R necessária para extrair os nomes das funções. O Regex também é simples e, para um arquivo existente, é muito fácil verificar - apenas cumprimente a palavra functione verifique se cada ocorrência exibida segue a regra.

  • não há necessidade de executar a fonte. É totalmente estático .

  • em muitos casos, você não precisará alterar o arquivo de origem e, em outros, haverá alterações mínimas. Se você estiver escrevendo o script do zero, com isso em mente, é ainda mais fácil organizar.

Existem muitas outras alternativas ao longo da ideia de convenções. você pode ter um regex mais sofisticado ou adicionar # FUNCTIONno final da primeira linha de qualquer definição de função se estiver escrevendo o script do zero e depois cumprimentar essa frase e extrair a primeira palavra da linha, mas a principal sugestão aqui parece particularmente atraente devido à sua simplicidade e às outras vantagens listadas.

Teste

# 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
fonte
lapply(x, function(y) dostuff(y))iria quebrar isso
alan ocallaghan 15/11/19
@alan ocallaghan, Seu exemplo viola as regras declaradas e, portanto, não pode ocorrer validamente. Para escrever isso e ainda permanecer dentro das regras, seria necessário iniciar a função em uma nova linha recuada ou poderia ser necessário recuar a lapply.
G. Grothendieck
Eu acho que o utilitário é maciçamente degradado se você precisar de formatação específica, uma vez que pode exigir alterar o arquivo - e nesse caso, você pode também sugere que o usuário leia os nomes das funções manualmente
alan Ocallaghan
11
Isso é apenas uma consideração se você não controla o arquivo, mas excluímos essa possibilidade. O uso de convenções é muito comum na programação. Costumo colocar # TODOtodo o meu código para que eu possa cumprimentar minhas tarefas, por exemplo. Outra possibilidade ao longo das mesmas linhas seria escrever # FUNCTIONno final da primeira linha de qualquer definição de função.
G. Grothendieck
11
tentando fazer a análise com regex é a estrada para o inferno ....
TMS
0

Isso adapta o código usado na postagem do meu comentário para procurar uma sequência de tokens (símbolo, operador de atribuição e depois função), e deve pegar todas as funções definidas. Não tenho certeza se é robusto como resposta do MrFlick, mas é outra opção:

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"]]
}
Andrew
fonte