Разработчики пакета magrittr ставили перед собой следующие цели: сократить время разработки и сделать код более читаемым и лёгким в сопровождении. Мотивами для создания пакета послужили оператор |> в языке F# и конвейеры (pipes) в unix-подобных операционных системах.

magrittr содержит набор операторов и вспомогательных функций, которые позволяют:

  • избежать вложенных вызовов функций
  • минимизировать создание промежуточных переменных
  • организовать последовательности команд в единую цепочку

Основная идея конвейера — это подстановка левой части выражения (left-hand side, LHS) в качестве первого аргумента функции правой части (right-hand side, RHS). В качестве конвейера используется оператор %>%. Если левую часть выражения необходимо подставить не на первое место правой место, то можно использовать заменитель точку (.).

А теперь давайте подробнее рассмотрим возможности, которые предоставляет пакет magrittr.

Конвейер (оператор %>%)

Оператор %>%, как уже говорилось выше, подставляет правую часть выражения в качестве первого позиционного аргумента в функцию правой части выражения. Другими словами, выражение x %>% f эквивалентно выражению f(x), а выражение x %>% f(y), соответственно, выражению f(x, y). Более сложный пример: x %>% f %>% g %>% h эквивалентно h(g(f(x))).

Давайте рассмотрим два примера кода, использующие функции пакета dplyr: первый отформатирован традиционным образом — второй с помощью конвейеров.

hourly_delay <- filter( 
    summarise(
        group_by(
            filter(
                flights,
                !is.na(dep_delay)
            ),
            date, hour
        ),
        delay = mean(dep_delay),
        n = n()
    ),
    n > 10
)

Ниже приведён идентичный по функционалу код, оформленный с помощью конвейеров.

hourly_delay <- flights %>% 
    filter(!is.na(dep_delay)) %>% 
    group_by(date, hour) %>% 
    summarise( 
        delay = mean(dep_delay), 
        n = n() ) %>% 
    filter(n > 10)

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

Подстановка аргумента (заменитель .)

Левая часть выражения может быть заменена на точку (.), если необходимо подставить левую часть в качестве не первого позиционного аргумента или использовать левую часть выражения несколько раз. Выражение x %>% f(y, .) эквивалентно f(y, x) или при использование имени аргумента, выражение x %>% f(y, z = .) будет эквивалентно f(y, z = x). Таким образом, точку (.) мы можем ставить туда куда нам нужно и столько раз сколько нам нужно. Например:

x %>% f(y = nrow(.), z = ncol(.))

Будет эквивалентно выражению:

f(x, y = nrow(x), z = nrow(x))

Точка (.) также может использоваться во вложенных вызовах функций:

1:5 %>% paste(letters[.])
#> [1] "1 a" "2 b" "3 c" "4 d" "5 e"

При этом, когда мы используем точку (.) во вложенных вызовах, она используется в дополнение, а не вместо первого аргумента. Например, предыдущее выражение эквивалентно следующему:

1:5 %>% paste(., letters[.])
#> [1] "1 a" "2 b" "3 c" "4 d" "5 e"

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

1:5 %>% { paste(letters[.]) }
#> [1] "a" "b" "c" "d" "e"

Стоит также отметить, что заменитель точка (.) не конфликтует с точкой, применяемой в формулах для обозначения остальных предикторов:

npk %>% aov(yield ~., data = .) %>% summary
#>             Df Sum Sq Mean Sq F value Pr(>F)
#> block        5    343    68.7    4.29 0.0127
#> N            1    189   189.3   11.82 0.0037
#> P            1      8     8.4    0.52 0.4800
#> K            1     95    95.2    5.95 0.0277
#> Residuals   15    240    16.0

Или просто

npk %>% aov(formula = yield ~.) %>% summary
#>             Df Sum Sq Mean Sq F value Pr(>F)
#> block        5    343    68.7    4.29 0.0127
#> N            1    189   189.3   11.82 0.0037
#> P            1      8     8.4    0.52 0.4800
#> K            1     95    95.2    5.95 0.0277
#> Residuals   15    240    16.0

Правда, здесь нам приходится использовать имя аргумента (formula), т.к. левая часть выражения (в данном случае датасет npk) будет подставлена в качестве значения первого аргумента, а это аргумент formula. Если мы передадим вместо формулы датасет, то это приведёт к ошибке. Поскольку мы уже использовали аргумент formula явно, наш датасет будет передан второму позиционному аргументу функции aov(), которым является аргумент data.

Создание функций и лямбда-выражения

Пакет margittr также позволяет легко создавать простые функции с одним аргументом с помощью точки (.) и оператора %>%:

f <- . %>% cos %>% sin

Это выражение эквивалентно:

f <- function(.) sin(cos(.))

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

Например, при использовании lapply() функцию можно создать следующим образом:

dataset %>%
    lapply(. %>% abs %>% mean(na.rm = TRUE))

Это выражение можно также записать традиционным синтаксисом:

lapply(dataset, function(.) mean(abs(.), na.rm = TRUE))

Когда нам необходимо произвести ряд промежуточных расчётов очень, часто используют анонимные функции. Например:

iris %>%
    {
        n <- sample(1:10, size = 1)
        H <- head(., n)
        T <- tail(., n)
        rbind(H, T)
    } %>%
    summary

Чтобы лучше понять предыдущее выражение, перепишем его следующим образом:

iris %>%
    (function(.) {
        n <- sample(1:10, size = 1)
        H <- head(., n)
        T <- tail(., n)
        rbind(H, T)
    }) %>%
    summary

Если бы мы использовали стандартный синтаксис, то наш код получился бы примерно следующим:

summary({
    n <- sample(1:10, size = 1)
    H <- head(iris, n)
    T <- tail(iris, n)
    rbind(H, T)
})

Присвоение (оператор %<>%)

Пакет magrittr предлагает ещё несколько операторов, которые используются ситуативно. Один из таких операторов призван минимизировать ситуации, когда мы вынуждены модифицировать уже существующую переменную. Поэтому вместо конструкции x <- fun(x) мы можем использовать конструкцию x %<>% fun. Ещё пример:

x %<>% abs %>% sort

Традиционный подход:

x <- sort(abs(x))

В сочетании с оператором %>%, данный оператор очень полезен при длинных цепочках преобразований, которые довольно часто встречаются при преобразовании данных (пакеты dplyr, tityr).

«Тройник» (оператор %T%)

%T% — ещё один оператор, который используется когда на одном из этапов используются функции или выражения, результат работы которых нам дальше по цепочке передавать не нужно. Таким образом, у нас получается как бы «тройник», когда на одном из этапов происходит ответвление, после чего цепочка продолжается дальше. Это удобно, напрмиер, для построения графиков, что позволяет не разрывать последовательность команд.

Приведём пример:

rnorm(200) %>%
    matrix(ncol = 2) %T>%
    plot %>%
    colSums

Это выражение эквивалентно следующему:

rnorm(200) %>%
    matrix(ncol = 2) %>%
    { plot(.); . } %>%
    colSums

Если бы мы использовали традиционный синтаксис, нам пришлось бы выполнить несколько команд с созданием промежуточной пременной:

m <- matrix(rnorm(200), ncol = 2)
plot(m)
colSums(m)

Экспозиция переменных (оператор %$%)

Последний оператор в пакете magrittr — это %$%, который является аналогом функции with(). Функция with() создаёт локальное окружение (envirinment) из дата фрейма и выполняет выражения в нём т.е. позволяет использовать элементы списка или дата фрейма как обычные переменные. Обратите внимание, если столбцы содержат недопустимые символы (например, пробелы, дефисы и т.д.), то нужно заключить их в обратные кавычки: dataset %>% cor(Первый столбец,Второй столбец).

Приведём несколько примеров.

iris %>%
    subset(Sepal.Length > mean(Sepal.Length)) %$%
    cor(Sepal.Length, Sepal.Width)

Это выражение можно переписать с использованием функции with() следующим образом:

with(subset(iris, Sepal.Length > mean(Sepal.Length)),
     cor(Sepal.Length, Sepal.Width))

Также удобно использовать непосредственно имена столбцов при построении графиков:

data.frame(z = rnorm(100)) %$%
    ts.plot(z)

Аналогичное выражение с with():

with(data.frame(z = rnorm(100)),
     ts.plot(z))

Ещё один пример:

npk %$% aov(yield ~ N * P * K) %>% summary

Правда, использовать точку (.) непосредственно в формуле уже не получится.

Вспомогательные функции

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

rnorm(100) %>% `*`(5) %>% `+`(5) %>% mean
#> [1] 5.049

“Как мы видим, в коде с использованием операторов magrittr, бинарные операторы наподобие * необходимо экранировать кавычками. Это создает очевидные неудобства, поэтому в пакете margittr предлагается несколько псевдонимов для этих операторов. Так, в вышеприведенном выражении оператор * необходимо заменить на multiply_by:

rnorm(100) %>% multiply_by(5) %>% add(5) %>% mean
#> [1] 5.012

Поэтому пакет magrittr предоставляет ряд псевдонимов (aliases) для распространённых бинарных операторов. Полный их список доступен с помощью команды

help(topic = "extract", package = "magrittr")

Или просто ?extract, если до этого вы уже загрузии пакет magrittr.