Зачем использовать purrr:: map вместо lapply?
Есть ли причина, по которой я должен использовать
map(<list-like-object>, function(x) <do stuff>)
вместо
lapply(<list-like-object>, function(x) <do stuff>)
вывод должен быть таким же, и результаты тестов, которые я сделал, показывают, что lapply
немного быстрее (он должен быть как map
должен оценивать все входные данные нестандартной оценки).
Итак, есть ли причина, почему для таких простых случаев я должен действительно рассмотреть возможность переключения на purrr::map
? Я не спрашиваю здесь о симпатиях или симпатиях к синтаксису, других функциях, предоставляемых purrr и т.д., Но строго о сравнении purrr::map
с lapply
, предполагая использование стандартной оценки, т.е. map(<list-like-object>, function(x) <do stuff>)
. Есть ли преимущество, которое purrr::map
имеет в плане производительности, обработки исключений и т.д.? Комментарии ниже показывают, что это не так, но, может быть, кто-то может разработать немного больше?
Ответы
Ответ 1
Если единственной функцией, которую вы используете из purrr, является map()
, то нет, преимущества не существенны. Как указывает Рич Пауло, главное преимущество map()
- это помощники, которые позволяют писать компактный код для общих особых случаев:
-
~. + 1
~. + 1
эквивалентно function(x) x + 1
-
list("x", 1)
эквивалентен function(x) x[["x"]][[1]]
. Эти помощники являются более общими, чем [[
- смотрите ?pluck
для деталей. Для прямоугольника .default
аргумент .default
особенно полезен.
Но в большинстве случаев вы не используете одну функцию *apply()
/map()
, вы используете их несколько, и преимущество purrr заключается в гораздо большей согласованности функций. Например:
-
Первый аргумент lapply()
- это данные; первым аргументом mapply()
является функция. Первым аргументом для всех функций карты всегда являются данные.
-
С помощью vapply()
, sapply()
и mapply()
вы можете подавить имена в выводе с помощью USE.NAMES = FALSE
; но lapply()
не имеет этого аргумента.
-
Не существует согласованного способа передачи согласованных аргументов в функцию mapper. Большинство функций используют ...
но mapply()
использует MoreArgs
(который вы ожидаете назвать MORE.ARGS
), а Map()
, Filter()
и Reduce()
ожидают, что вы создадите новую анонимную функцию. В функциях карты константный аргумент всегда идет после имени функции.
-
Почти каждая функция purrr является стабильной по типу: вы можете предсказать тип вывода исключительно из имени функции. Это не так для sapply()
или mapply()
. Да, есть vapply()
; но нет эквивалента для mapply()
.
Вы можете подумать, что все эти второстепенные различия не важны (так же, как некоторые люди думают, что нет никаких преимуществ по сравнению со строковыми регулярными выражениями R), но по моему опыту они вызывают ненужные трения при программировании (различные порядки аргументов всегда используются для отключения мне), и они затрудняют изучение методов функционального программирования, потому что помимо больших идей, вам также нужно изучить кучу случайных деталей.
Purrr также заполняет некоторые удобные варианты карт, которые отсутствуют в базе R:
-
modify()
сохраняет тип данных, используя [[<-
для изменения "на месте". В сочетании с вариантом _if
это позволяет (красивый IMO) код, такой как modify_if(df, is.factor, as.character)
-
map2()
позволяет отображать одновременно на x
и y
. Это облегчает выражение идей, таких как map2(models, datasets, predict)
-
imap()
позволяет отображать одновременно x
и его индексы (имена или позиции). Это облегчает (например) загрузку всех csv
файлов в каталог, добавляя к каждому столбец filename
.
dir("\\.csv$") %>%
set_names() %>%
map(read.csv) %>%
imap(~ transform(.x, filename = .y))
-
walk()
возвращает входные данные невидимо; и полезно, когда вы вызываете функцию из-за ее побочных эффектов (т.е. записи файлов на диск).
Не говоря уже о других помощниках, таких как safely()
и partial()
.
Лично я нахожу, что когда я использую purrr, я могу писать функциональный код с меньшим трением и большей легкостью; это уменьшает разрыв между продумыванием идеи и ее реализацией. Но ваш пробег может отличаться; нет необходимости использовать purrr, если это не поможет вам.
Microbenchmarks
Да, map()
немного медленнее, чем lapply()
. Но стоимость использования map()
или lapply()
зависит от того, что вы отображаете, а не от затрат на выполнение цикла. Микробенчмарк ниже показывает, что стоимость map()
по сравнению с lapply()
составляет около 40 нс на элемент, что вряд ли окажет существенное влияние на большую часть кода R.
library(purrr)
n <- 1e4
x <- 1:n
f <- function(x) NULL
mb <- microbenchmark::microbenchmark(
lapply = lapply(x, f),
map = map(x, f)
)
summary(mb, unit = "ns")$median / n
#> [1] 490.343 546.880
Ответ 2
Сравнение purrr
и lapply
сводится к удобству и скорости.
1. purrr::map
синтаксически удобнее, чем lapply
извлечь второй элемент списка
map(list, 2)
который как @F. Приве указал, так же, как:
map(list, function(x) x[[2]])
с lapply
lapply(list, 2) # doesn't work
нам нужно передать анонимную функцию...
lapply(list, function(x) x[[2]]) # now it works
... или, как указал @RichScriven, мы передаем [[
в качестве аргумента в lapply
lapply(list, '[[', 2) # a bit more simple syntantically
Поэтому, если вы обнаружите, что применяете функции ко многим спискам, используя lapply
, и устали либо определять пользовательскую функцию, либо писать анонимную функцию, удобство - это одна из причин, чтобы перейти к purrr
.
2. Специфичные для типа карты функции просто много строк кода
-
map_chr()
-
map_lgl()
-
map_int()
-
map_dbl()
-
map_df()
- мой любимый, возвращает фрейм данных.
Каждая из этих картографических функций конкретного типа возвращает атомарный список (вектор), а не списки, возвращаемые map()
и lapply()
. Если вы имеете дело с вложенными списками атомарных векторов внутри, вы можете использовать эти специфичные для типа функции отображения для непосредственного извлечения векторов и принудительного преобразования векторов непосредственно в векторы int, dbl, chr. Базовая версия R будет выглядеть примерно так: as.numeric(sapply(...))
, as.character(sapply(...))
и т.д. Это дает purrr
еще один момент для удобства и функциональности.
3. Удобство в стороне, lapply
, [немного] быстрее, чем map
Используя удобные функции purrr
, как @F. Приве отметил, что немного замедляет обработку. Пусть гонятся за каждым из 4 случаев, которые я представил выше.
# devtools::install_github("jennybc/repurrrsive")
library(repurrrsive)
library(purrr)
library(microbenchmark)
library(ggplot2)
mbm <- microbenchmark(
lapply = lapply(got_chars[1:4], function(x) x[[2]]),
lapply_2 = lapply(got_chars[1:4], '[[', 2),
map_shortcut = map(got_chars[1:4], 2),
map = map(got_chars[1:4], function(x) x[[2]]),
times = 100
)
autoplot(mbm)
![enter image description here]()
И победитель....
lapply(list, '[[', 2)
В общем, если вам base::lapply
скорость: base::lapply
(хотя она не намного быстрее)
Для простого синтаксиса и выразимости: purrr::map
Это превосходное purrr
пособие по purrr
подчеркивает удобство отсутствия явной записи анонимных функций при использовании purrr
и преимущества map
функций, зависящих от типа.
Ответ 3
Если мы не рассматриваем аспекты вкуса (иначе этот вопрос должен быть закрыт) или согласованность синтаксиса, стиль и т.д., ответ отрицательный, нет специальной причины использовать map
вместо lapply
или других вариантов примените семейство, например, более строгий vapply
.
PS: Для тех, кто безвозмездно бежит, просто помните, что OP писал:
Я не спрашиваю здесь об одном симпатии или неприязнь к синтаксису, другие функции, предоставляемые purrr и т.д., но строго о сравнение purrr:: map с lapply, предполагающее использование стандартного оценка
Если вы не рассматриваете синтаксис и другие функции purrr
, нет никакой особой причины использовать map
. Я использую purrr
сам, и я в порядке с ответом Хэдли, но он иронизирует те самые вещи, о которых заявлял OP заранее, о которых он не спрашивал.