Синтаксис языка

Операторы

Присваивание (<-, <<-, =)

Основной оператор присваивания в R — это оператор <-. Например, x <- 5. Если до этого объект не был создан, то таким образом мы создадим объект x, в котором содержится значение 5. Если до этого объект существовал, то ему будет присвоено новое значение (перезаписано). С формальной точки зрения, оператор <- связывает значение и символьную запись (название), с помощью которой можно обращаться к значению.

Оператор <- служит, наверное, одним из самых надёжных визуальных маркеров, позволяющих отличить код на R от кода на других языках. Такая форма оператора присваивания обусловлена историческим контекстом: в период разработки и использования в Bell Labs языка S (предшественника R) код писали на клавиатурах, на которых была одна клавиша для печати знака <-.

Несмотря на визуальную понятность оператора, он нередко подвергается критике, в частности, за то, что даёт возможность ошибиться. Так, неправильно поставленный пробел меняет присвоение на сравнение: выражения x <- 5 и x < -5 различаются одним неправильно поставленным пробелом, однако содержательно они совершенно разные.

Оператор = аналогичен оператору <- и может быть использован для левостороннего присваивания. Одновременно оператор = используется при указании значений аргументов функций. Большинство руководств по стилю рекомендуют именно такое использование операторов <- и =: первый для присваивания, а второй — при использовании функций.

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

f1 <- function(x) {
  tmp1 <<- paste('the result of <<- operator in f1()')
  tmp2 <- x * 2
  return(tmp2)
}

print(f1(2))
## [1] 4
print(tmp1)
## [1] "the result of <<- operator in f1()"

Вместо оператора <<- для изменения значений объектов в различных окружениях рекомендуется использовать функцию assign(). В этой функции с помощью аргумента envir можно указать, в каком окружении происходит присвоение. Окружением может быть как пользовательское окружение, так и родительское (parent.env())/глобальное (.GlobalEnv):

f2 <- function(x) {
  assign(x = 'tmp1', value = paste('the result of assign() in f2()'), envir = .GlobalEnv)
  tmp2 <- x * 2
  return(tmp2)
}

print(f2(2))
## [1] 4
print(tmp1)
## [1] "the result of assign() in f2()"

Несмотря на то, что операторы присваивания позволяют множественное присваивание, а операторы -> и ->> — ещё и правостороннее присваивание, выражения вида x = y = z = 5 или 5 <- x -> y лучше не использовать, так как они уменьшают читабельность и прозрачность кода.

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

# создаём и выводим на печать таблицу с колонками var1, var2
new_df <- data.frame(var1 = 1:3, var2 = letters[1:3])
print(new_df)
##   var1 var2
## 1    1    a
## 2    2    b
## 3    3    c
# меняем названия колонок на col1, col2 и выводим таблицу
names(new_df) <- c('col1', 'col2')
print(new_df)
##   col1 col2
## 1    1    a
## 2    2    b
## 3    3    c

В настоящее время методы и выражения вида names<- характерны скорее для достаточно старого кода, написанного на базовом R.

Арифметические действия

Арифметические операторы аналогичны обычным математическим операторам:

  • +, - — бинарные и унарные сложение и вычитание;
  • *, / — умножение и деление;
  • ^ или ** — возведение в степень;
  • %/% — деление нацело;
  • %% — остаток при делении нацело.

Сравнение и логические операторы

Операторы сравнения или логические операторы обычно используются при необходимости указать какие-то условия для выбора элементов вектора или таблицы, а также в конструкциях if/else (см. Условные операторы).

Операторы сравнения:

  • <, <= — меньше, меньше или равно;
  • >, >= — больше, больше или равно;
  • ==, != — равно и не равно.

Логические операторы:

  • ! — отрицание, например, !is.na(x);
  • &, &&И, бинарное И (оператор & поэлементно объединяет элементы двух множеств, оператор && объединяет только первые слева элементы каждого множества), например, объединяем результаты двух логических проверок:
# объединяются попарно TRUE TRUE и FALSE FALSE
c(3, 5) < 6 & c(7, 3) < 6
## [1] FALSE  TRUE
# объединяются только первые элементы множеств — TRUE и FALSE
c(3, 5) < 6 && c(7, 3, 6) < 6
## [1] FALSE
  • |, ||ИЛИ, бинарное ИЛИ (аналогично операторам & и &&, обычно бинарные И/ИЛИ используются разработчиками при написании функций, чтобы исключить возможность неоднозначности операции).

Доступ к данным

Как правило, объекты в R — это таблицы, списки или сложные объекты с большим количеством вложенных элементов. Для того чтобы извлечь какой-то элемент или переписать его значение, используются операторы доступа. Здесь мы просто перечислим и дадим общее представление об этих операторах, подробнее операторы доступа к данным, особенно операторы [ и [[, будут рассмотрены в разделе про манипуляции с данными.

$ и @

Оператор $ используется для обращения к именованному элементу списка, колонке датафрейма и прочим элементам структуры объектов. Вызов выглядит следующим образом: <object_name>$<element_name>. В выражении можно использовать несколько операторов, особенно часто это необходимо при работе со сложными вложенными списками и объектами S3- или R6-классов. Оператор $ обладает также полезной особенностью: название элемента можно вводить не полностью, но введённая часть должна однозначно определять требуемый элемент. Выведем первые шесть строк (функция head()) датафрейма cars и отдельно первые шесть значений колонки speed с полным и частичным указанием названия колонки:

head(cars)
##   speed dist
## 1     4    2
## 2     4   10
## 3     7    4
## 4     7   22
## 5     8   16
## 6     9   10
head(cars$speed)
## [1] 4 4 7 7 8 9
head(cars$sp)
## [1] 4 4 7 7 8 9

Оператор @ используется аналогично оператору $ при работе с объектами S4-класса (доступ к слотам), однако требует полного названия требуемого элемента.

[ и [[

Оператор [ в определённой мере аналогичен оператору $, однако обладает более гибким функционалом. Так, с помощью оператора можно выделять элементы по их полному названию, по номеру или вектору номеров в списке элементов, по логическому условию:

tmp <- c(2, 5, 6, 9)
tmp[c(1, 3)]
## [1] 2 6

Для некоторых объектов оператор [ переопределён и имеет дополнительные аргументы.

Оператор [[ похож на операторы [ и $, однако на вход принимать может только одно значение — номер позиции или название элемента (по умолчанию название должно быть полным):

tmp <- c(v1 = 'alpha', v2 = 'beta') 
tmp['v1']
##      v1 
## "alpha"
tmp[['v1']]
## [1] "alpha"

Доступ к объекту в пространстве имён (::, :::)

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

Чаще всего потребность в этих операторах возникает тогда, когда какая-то функция из специального пакета используется однократно и подключать весь пакет нет необходимости. Например, установка пакета из github-репозитория с помощью пакета remotes: remotes::install_github("ropensci/plotly"). Также :: или ::: используются в ситуациях, когда в нескольких пакетах встречаются одинаковые названия функций или объектов. Например, fromJSON() встречается одновременно в пакетах jsonlite, RJSONIO и rjson.

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

Иерархия операторов

Операторы организованы в иерархию. Это позволяет сочетать их в разных выражениях. При этом некоторые результаты выполнения некоторых выражений могут быть весьма неожиданными, если забывать про иерархию. Так, результатом 3 ^ 1:3 будет 3, потому что оператор возведения в степень (^) выполняется первым и только потом применяется оператор последовательности (:).

Список операторов по убыванию приоритета выполнения:

Оператор Значение
::, ::: доступ к значениям в пространстве имён
$, @ доступ к элементу объекта/слоту по названию
[, [[ доступ к элементу объекта/слоту по индексу
^ возведение в степень
-, + унарные плюс и минус
: оператор последовательности (короткая запись для seq())
%any% специальные операторы (включая %% и %/%)
*, / умножение и деление
+, - сложение и вычитание
<, >, <=, >=, ==, != логическое сравнение
! логическое отрицание
&, && логическое И
|, || логическое ИЛИ
~ знак формулы
->, ->> правое присваивание
<-, <<- левое присваивание
= левое присваивание
?, ?? справка

Конвейеры

%>%

Оператор конвейера %>% не входит в базовые библиотеки R и принадлежит пакету magrittr, однако весьма распространён, в частности, широко используется в пакетах экосистемы tidyverse. Оператор %>% используется для того, чтобы связать в последовательность несколько операций, когда в последующее выражение передаётся результат предыдущего. В результате сокращается количество кода и/или повышается читабельность. Например, три идентичных вычисления, первое использует оператор конвейера, второе — вложенные выражения, а третье просто каждую операцию производит в отдельном выражении:

library(magrittr)

x <- 12 ^ 2 %>% sqrt %>% -2

x <- sqrt(12^2) - 2

x <- 12
x <- x ^ 2
x <- sqrt(x)
x <- x - 2

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

|>

Оператор конвейера |> совсем недавно вошел в base R. Написание взято от классического символа конвейера, используемого в bash/awk и подобных языках. Также |> используется в некоторых других высокоуровневых языках, например, в F#. Операторы %>% и |> одинаковы по смыслу, но всё же неравнозначны. Собственно, поэтому команда R Core и не стала использовать %>% для своего конвейера.

Основное различие заключается в том, что они выполняются в разное время: %>% применяется при выполнении выражений, в рантайме; оператор |> обрабатывается интерпретатором на этапе чтения кода, и только потом происходит выполнение конвейера выражений. Такая организация делает применение |> надёжным и не вносящим дополнительных расходов.

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

lobstr::ast(g(f(x)))
## █─g 
## └─█─f 
##   └─x
lobstr::ast(x |> f() |> g())
## █─g 
## └─█─f 
##   └─x

Вызов функций

Классический вызов

Функции составляют основу языка R — все операции совершаются с использованием функций, будь то действие с объектом, вывод графика или печать значения объекта.

Вызов функции выглядит следующим образом: function_name(arglist). Например:

mean(x = 1:10)
## [1] 5.5

В приведённом примере mean — это название функции, а x — название аргумента функции. Функция в примере вычисляет среднее значение по вектору значений аргумента x.

Полный список аргументов функции можно посмотреть в справке по функции. Список аргументов функции импорта csv-файлов выглядит следующим образом (можно посмотреть в справке или воспользоваться функцией args()):

args(read.csv)
## function (file, header = TRUE, sep = ",", quote = "\"", dec = ".", 
##     fill = TRUE, comment.char = "", ...) 
## NULL

При вызове функции аргументы можно задать разными способами. Первый способ — по названию, то есть надо однозначно указывать название аргумента и его значение: read.csv(file = 'myfile.csv'). Второй способ — позиционный, когда значения аргументов перечислены в функции в том же порядке, как они объявлены в документации: read.csv( 'myfile.csv', FALSE, ';').

В приведённом примере документации функции read.csv() у ряда аргументов уже заданы значения, например, header = TRUE. Эти значения называются значения аргументов по умолчанию, и если при вызове функции для этих аргументов не указаны другие значения, то используются именно заданные по умолчанию. Это позволяет корректно выполнять выражение read.csv(file = 'myfile.csv') (или read.csv('myfile.csv')), несмотря на то, что значение задано только для одного аргумента, file.

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

f1 <- function(x, ...) {
  x <- x^2
  return(..1)
}
f1(x = 3, 5, 9)
## [1] 5

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

Вызов анонимных функций

Вызов анонимных функций в R выглядит следующим образом: вместо названия функции мы пишем function(x) { some operations with x }. Часто анонимные функции используются в сочетании с функциями семейства *pply (подробнее в разделе по созданию функций):

sapply(X = 1:5, FUN = function(x) x + 1)
## [1] 2 3 4 5 6

С относительно недавнего времени эту запись можно немного сократить и вместо function(x) использовать \(x). Символ \ был выбран в данном случае как напоминающий символ \(\lambda\), используемый для обозначения лямбда-выражений (аналоги анонимных функций в R) в функциональных языках.

Служебные символы

Конец выражения

Для маркировки конца выражения в R используется ;. Однако парсер интерпретатора читает код построчно. Это значит, что если не писать более одного выражения на одной строке, то указывать ; нет необходимости, достаточно переноса строки. В результате вот такие два выражения идентичны:

# два выражения на одной строке
x <- 5; y <- 3
x + y
## [1] 8
# два выражения отдельно
x <- 5
y <- 3
x + y
## [1] 8

Фигурные скобки

Фигурные скобки используются в тех ситуациях, когда необходимо несколько выражений объединить в один блок. Чаще всего это необходимо при объявлении функций, в циклах и в некоторых случаях при использовании условных операторов (if … else/ifelse/switch, см. Условные операторы). Результатом выполнения блока будет результат последнего выражения:

result <- {
  y <- 2 * 3
  z <- y ^ 2
  x <- y + z
}
print(result)
## [1] 42

В том случае, когда тело функции или цикла составляет одно выражение, фигурные скобки можно опустить. В таком случае выражение после function()/for на той же строке или одна следующая строка считается как относящиеся к функции/циклу:

for (i in 1:3) 
  print(i)
## [1] 1
## [1] 2
## [1] 3

Комментарии

В R для комментирования используется символ #. Если в какое-то место на строке поставить знак комментария, то вся оставшаяся часть строки будет считаться закомментированной. Исключение — если знак комментария находится внутри выражения, окруженного кавычками:

x <- 5 # это комментарий
x <- "# а вот это уже не комментарий"

Обычно строку комментария ставят перед комментируемым выражением. Однако если комментарий короткий, то можно, как и в примере выше, писать комментарий после комментируемого выражения, на той же строке.

На данный момент R не поддерживает комментирование блоками, как это можно делать в других языках с помощью выражения вида /* commented lines of code */, поэтому каждую строчку надо комментировать отдельно.

За пределами базового R, но в сопутствующих пакетах, знак # может иметь иные значения, например, в RMarkdown он используется для создания заголовков и внутренних ссылок.

Кавычки

В R используются три вида кавычек: кавычки (“), апостроф (’) и обратный апостроф (`). Простые кавычки и апостроф взаимозаменяемы, в коде можно использовать и то, и другое, в том числе и комбинированно. Единственное, следует выдерживать стилистическую однородность и использовать какой-нибудь тип как основной, а второй — только в тех ситуациях, когда надо написать сложное выражение. Например, такие ситуации возникают при написании запроса к SQL-базе данных: в SQL, как правило, апострофы используются для текстовых значений и дат, а кавычки — для названий таблиц: dbGetQuery(con, "select * from mytable where dt >= '2018-01-01'").

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

`это кириллица` <- 'объект с кириллическим названием и пробелом'
print(`это кириллица`)
## [1] "объект с кириллическим названием и пробелом"

Помимо обращения к объектам со специфичным названием, обратный апостроф может использоваться при создании собственных операторов:

`%~%` <- function(x, y) 
  paste("new operator's result:", x,  "^", y, "=", x ^ y)
2 %~% 3
## [1] "new operator's result: 2 ^ 3 = 8"

Также обратный апостроф используется в rmarkdown-разметке для выделения фраз или создания коротких исполняемых выражений в самом тексте, а также для создания отдельных чанков кода. Например, так в тексте выглядит выражение `x <- 2 * 9`: x <- 2 * 9.

Зарезервированные слова

Как и во многих других языках, в R часть слов является зарезервированными, то есть за ними закреплено определённое значение (функционал), и пользователь не имеет возможности использовать их в качестве имён объектов. Основные зарезервированные слова (согласно документации R Foundation) можно посмотреть с помощью ?Reserved:

if else repeat while function for in next break
TRUE FALSE NULL Inf NaN
NA NA_integer_ NA_real_ NA_complex_ NA_character_
... ..1 ..2 etc

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