Получить все источники функций

11

В R я использую source()для загрузки некоторых функций:

source("functions.R")

Можно ли получить список всех функций, определенных в этом файле? Как имена функций. (Может быть, source()сам может это как-то вернуть?).

PS: в крайнем случае можно было бы вызвать source()второй раз, как, local({ source(); })а затем выполнить ls()внутри и фильтровать функции, но это слишком сложно - есть ли более простое и менее неуклюжее решение?

TMS
источник
1
Это не использует source(), но эта старая тема может быть интересна для вас.
Андрей
1
@ Andrew спасибо, я проверил предложенные решения, но это звучит намного безумнее, чем последнее средство, которое я представил в вопросе :)
TMS
2
Я не знаю, это решение envir <- new.env() source("functions.R", local=envir) lsf.str(envir)
безумнее
2
Сделайте пакет из ваших исходных файлов. Тогда вы получите все преимущества, включая пространство имен пакета.
Роланд
@TMS, неправильно понял ваш вопрос / не прочитал, что вы хотели определенные функции. Извиняюсь!
Андрей

Ответы:

7

Я думаю, что лучшим способом было бы поместить файл во временную среду. Запросите эту среду для всех функций, затем скопируйте эти значения в родительскую среду.

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
источник
спасибо, это решение выглядит многообещающим, как единственное прямо сейчас! Удивительно, но тот, у кого меньше голосов. Это тот, который я упомянул в качестве последнего средства, но использующий new.env()вместо элегантного, local({ })который я не уверен, будет ли он работать с assignродительским фреймом.
TMS
1) вы думаете, это будет работать local()? И кстати, 2) что вы делаете в цикле for: не существует ли какой-либо функции для объединения сред?
TMS
1
@TMS Это может работать с локальными, хотя я не пробовал. Я не знаю другого способа скопировать все переменные из одной среды в другую. Это не обычная операция.
MrFlick
Я думаю, что attachможно привыкнуть к тому, чтобы присоединить одну среду к другой. Хотя вы должны использовать posаргумент, а не указывать parent.frame. И это будет хорошо работать только для копирования всей среды, forцикл MrFlick позволяет копировать только функции поверх.
Грегор Томас
5

Это немного неуклюже, но вы можете посмотреть на изменения в объектах до и после sourceвызова следующим образом.

    # 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"
Андрей Чисхолм
источник
Спасибо! У меня тоже была эта идея, но она не работает по очень простой причине - если пакет уже был загружен (что происходит все время, когда я отлаживаю код, я просто повторно получаю исходники), то он ничего не возвращает.
TMS
3

Я думаю, что это регулярное выражение улавливает почти все допустимые типы функций (бинарный оператор, функции присваивания) и каждый действительный символ в имени функции, но, возможно, я пропустил крайний случай.

# 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<-`"
Алан Одкаллаган
источник
1
К вашему сведению, это не очень хорошее решение, но это определенно забавное решение. Я, вероятно, преобразовал бы файл в пакет, если бы мне действительно нужна эта информация.
алан ocallaghan
Я пропустил два крайних случая! Функции могут начинаться с .и функцией присваивания ( `foo<-`<- function(x, value)существует.
алан ocallaghan
Я использую =для назначения, это не поймает ни одну из моих функций ...
Грегор Томас
Хороший улов - отредактировано. Отмечу, что R позволяет делать глупости, подобные тем, ` d d` <- function(x)которые в данный момент не пойманы. Я не хочу, чтобы регулярное выражение стало слишком глупым, хотя я мог бы вернуться к нему.
алан ocallaghan
Кроме того , можно назначить функции с assign, <<-и ->. И будет очень трудно сделать так, чтобы этот подход учитывал функции, которые определены внутри функций, но на самом деле не находятся в исходной среде. Ваш ответ должен работать очень хорошо для стандартных случаев, но на самом деле вы не хотите писать R-парсер без регулярных выражений.
Грегор Томас
1

Если это ваш собственный сценарий, и вы можете контролировать его форматирование, достаточно простого соглашения. Просто убедитесь, что каждое имя функции начинается с первого символа в строке и что слово functionтакже появляется в этой строке. Любое другое использование слова functionдолжно появляться в строке, которая начинается с пробела или табуляции. Тогда однострочное решение:

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

Преимущества этого подхода в том, что

  • это очень просто . Правила просто изложены, и есть только одна простая строка кода R, необходимая для извлечения имен функций. Regex также прост, и для существующего файла это очень легко проверить - просто наберите слово functionи проверьте, соответствует ли каждое отображаемое вхождение правилу.

  • нет необходимости запускать источник. Это полностью статично .

  • во многих случаях вам вообще не нужно менять исходный файл, а в других случаях изменения будут минимальными. Если вы пишете сценарий с нуля с учетом этого, его еще проще организовать.

Есть много других альтернатив вдоль идеи конвенций. у вас может быть более сложное регулярное выражение или вы можете добавить # FUNCTIONв конце первой строки определения любой функции, если вы пишете скрипт с нуля, а затем извлекаете эту фразу и извлекаете первое слово в строке, но основное предложение здесь кажется особенно привлекателен благодаря своей простоте и другим перечисленным преимуществам.

Тестовое задание

# 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"
Г. Гротендик
источник
lapply(x, function(y) dostuff(y))сломал бы это
алан
@alan ocallaghan, Ваш пример нарушает установленные правила, поэтому он не может иметь место. Чтобы написать это и при этом оставаться в рамках правил, нужно запустить функцию с новой строки с отступом или с отступом в строке.
Г. Гротендик
Я думаю, что утилита сильно ухудшается, если вам требуется определенное форматирование, поскольку для этого может потребоваться изменение файла - в этом случае вы можете также предложить пользователю прочитать имена функций вручную
alan
1
Это только соображение, если вы не контролируете файл, но мы исключили эту возможность. Использование соглашений очень распространено в программировании. Я часто вставляю # TODOв свой код, чтобы я мог, например, выполнить свои задачи. Другая возможность в том же духе будет записана # FUNCTIONв конце первой строки любого определения функции.
Г. Гротендик
1
попытка сделать разбор с помощью регулярных выражений - это дорога в ад ....
TMS
0

Это адаптирует код, использованный в посте из моего комментария, для поиска последовательности токенов (символ, оператор присваивания, затем функция), и он должен захватывать любые определенные функции. Я не уверен, что он надежен как ответ MrFlick, но это еще один вариант:

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"]]
}
Эндрю
источник