Рекурсивная индексация списков со значением переменной индекса на шаг рекурсии - PullRequest
0 голосов
/ 18 октября 2019

Пух ... даже попытка правильно оформить заголовок уже доставляет мне головную боль.

У меня есть config.yml с вложенными значениями, и я хотел бы определить функцию индексацииget_config(), который принимает "подобные пути" строки значений.

«Объекты пути» строки значения соответствуют структуре вложенных объектов файла конфигурации. Основываясь на значении в виде пути, функция должна затем пойти и извлечь соответствующий объект иерархии («ветви» или «листья») из файла конфигурации.

Пример

Предположим, что этоСтруктура config.yml:

default:
  column_names:
    col_id: "id"
    col_value: "value"
  column_orders:
    data_structure_a: [
      column_names/col_id,
      column_names/col_value
    ]
    data_structure_b: [
      column_names/col_value,
      column_names/col_id
    ]

Вот проанализированная версия, с которой вы можете поиграться:

x <- yaml::yaml.load(
'default:
  column_names:
    col_id: "id"
    col_value: "value"
  column_orders:
    data_structure_a: [
      column_names/col_id,
      column_names/col_value
    ]
    data_structure_b: [
      column_names/col_value,
      column_names/col_id
    ]'
)

Доступ к объектам верхнего уровня легко с config::get(value):

config::get("column_names")
# $col_id
# [1] "id"
# 
# $col_value
# [1] "value"

config::get("column_orders")
# [1] "hello" "world"

Но я также хотел бы получить доступ к более глубоким объектам, например, column_names: col_id.

В псевдокоде:

config::get("column_names:col_id")

или

config::get("column_orders/data_structure_a")

Лучшее, что я мог придумать: полагаться на unlist()

get_config <- function(value, sep = ":") {
  if (value %>% stringr::str_detect(sep)) {
    value <- value %>% stringr::str_replace(sep, ".")
    configs <- config::get() %>% unlist()
    configs[value]
  } else {
    config::get(value)
  }
}

get_config("column_names")
# $col_id
# [1] "id"
#
# $col_value
# [1] "value"

get_config("column_names:col_id")
# column_names.col_id 
# "id" 

Хотя это и не элегантно, оно работает для большинства случаев использования, но не работает для неназванных объектов списка в файле конфигурации

get_config("column_orders:data_structure_a")
# <NA> 
#   NA 

, поскольку мой подход к индексированию не очень хорошо сочетается с результатом unlist() в неназванных списках:

config::get() %>% unlist()
# column_names.col_id          column_names.col_value 
# "id"                         "value" 
# column_orders.data_structure_a1 column_orders.data_structure_a2 
# "column_names/col_id"        "column_names/col_value" 
# column_orders.data_structure_b1 column_orders.data_structure_b2 
# "column_names/col_value"           "column_names/col_id" 

Таким образом, я бы хотел "пойти рекурсивно", но мой мозг говорит:"Ни за что, чувак"

Надлежащая проверка

Это решение близко (я полагаю).

Но я продолжаю думать, что мне нужно что-то вроде purrr::map2_if() или purrr::pmap_if() (которого AFAIK не существует) вместо purrr::map_if(), поскольку мне нужно не только рекурсивно просматривать список, стоящий за config::get(), но также выложена версия value (например, через stringr::str_split(value, sep) %>% unlist() %>% as.list())?

Ответы [ 2 ]

1 голос
/ 18 октября 2019

Вот одно из решений. Он использует некоторые внутренние функции из пакета R tfautograph. Вы можете использовать их по мере необходимости.

x <- yaml::yaml.load(
'default:
  column_names:
    col_id: "id"
    col_value: "value"
  column_orders:
    data_structure_a: [
      column_names/col_id,
      column_names/col_value
    ]
    data_structure_b: [
      column_names/col_value,
      column_names/col_id
    ]')


library(purrr)
library(magrittr)

leaf_names <- tfautograph:::leaf_names
pluck_structure <- tfautograph:::pluck_structure

get_leaf <- function(x, nm) {
  stopifnot(rlang::is_scalar_character(nm))

  full_leaf_paths <- leaf_names(x)
  i <- which.max(map_lgl(full_leaf_paths, ~any(. %in% nm)))

  leaf_path <- full_leaf_paths[[i]]
  leaf_path  %<>% .[1:which(. == nm)] 

  x[[leaf_path]]
} 


get_leaf(x, "column_names")
#> $col_id
#> [1] "id"
#> 
#> $col_value
#> [1] "value"
get_leaf(x, "column_orders")
#> $data_structure_a
#> [1] "column_names/col_id"    "column_names/col_value"
#> 
#> $data_structure_b
#> [1] "column_names/col_value" "column_names/col_id"
get_leaf(x, "data_structure_a")
#> [1] "column_names/col_id"    "column_names/col_value"
0 голосов
/ 19 октября 2019

Я придумал решение, основанное на Recall().

Однако, пытаясь найти здесь Интернет, вспоминаю, что читал где-то, что Recall() обычно не очень (память) эффективный способ сделать рекурсию в R? Также был бы признателен за дополнительные советы о том, как сделать рекурсию аккуратно с purrr и друзьями.

Содержимое файла конфигурации

Возможность вызова get_config() подразумевает, что у вас есть config.ymlфайл с указанным выше содержимым в корневом каталоге вашего проекта, заданный here::here(), но вы можете протестировать get_list_element_recursively() с помощью этого обходного пути:

x <- yaml::yaml.load('
  column_names:
    col_id: "id"
    col_value: "value"
  column_orders:
    data_structure_a: [
      column_names/col_id,
      column_names/col_value
    ]
    data_structure_b: [
      column_names/col_value,
      column_names/col_id
    ]
  nested_list:
    element_1:
      element_2:
        value: "hello world"
  ')

Функция defs

get_config <- function(value, sep = "/") {
  get_list_element_recursively(
    config::get(),
    stringr::str_split(value, sep, simplify = TRUE)
  )
}

get_list_element_recursively <- function(
  lst,
  el,
  .el_trace = el,
  .level_trace = 1
) {
  # Reached leaf:
  if (!is.list(lst)) {
    return(lst)
  }

  # Element not in list:
  if (!(el[1] %in% names(lst))) {
    message("Current list branch:")
    # print(lst)
    message(str(lst))
    message("Trace of indexing vec (last element is invalid):")
    message(stringr::str_c(.el_trace[.level_trace], collapse = "/"))
    stop(stringr::str_glue("No such element in list: {el[1]}"))
  }

  lst <- lst[[ el[1] ]]

  if (!is.na(el[2])) {
    # Continue if there are additional elements in `el` vec
    Recall(lst, el[-1], .el_trace, .level_trace = 1:(.level_trace + 1))
  } else {
    # Otherwise return last indexing result:
    lst
  }
}

Тестирование get_config()

get_config("column_names")
# $col_id
# [1] "id"
#
# $col_value
# [1] "value"

get_config("column_names/col_id")
# [1] "id"

get_config("column_names/col_nonexisting")
# Current list branch:
#   List of 6
# $ col_id                    : chr "id"
# $ col_value                 : chr "value"
#
# Trace of indexing vec (last element is invalid):
#   column_names/col_nonexisting
# Error in get_list_element_recursively(config::get(), stringr::str_split(value,  :
#     No such element in list: col_nonexisting

get_config("column_orders")
# $data_structure_a
# [1] "column_names/col_id"    "column_names/col_value"
#
# $data_structure_b
# [1] "column_names/col_value" "column_names/col_id"

get_config("column_orders/data_structure_a")
# [1] "column_names/col_id"    "column_names/col_value"

Тестирование get_list_element_recursively()

get_list_element_recursively(x, c("column_names"))
# $col_id
# [1] "id"
#
# $col_value
# [1] "value"

get_list_element_recursively(x, c("column_names", "col_id"))
# [1] "id"

get_list_element_recursively(x, c("column_names", "col_notthere"))
# Current list branch:
#   List of 2
# $ col_id   : chr "id"
# $ col_value: chr "value"
#
# Trace of indexing vec (last element is invalid):
#   column_names/col_notthere
# Error in get_list_element_recursively(x$default, c("column_names", "col_notthere")) :
#   No such element in list: col_notthere
...