Функции и создание функций

В R огромное количество готовых функций, написанных разработчиками ядра или пакетов. Однако нередко бывает необходимо написать собственную функцию. Причин их написания может быть много: не устраивают существующие, хочется убрать повторяющиеся куски из кода, много операций, которые выполняются итеративно и неоднократно и т. д. В таких случаях проще и лучше написать собственную функцию. Есть вполне очевидная рекомендация: если какая-то часть кода будет использоваться больше одного раза, возможно, её следует обернуть в функцию.

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

my_fun <- function(arg1, arg2) {
  # тело функции, операции, перемножаем переданные значения
  tmp <- arg1 * arg2
  
  # возвращаем результат
  return(tmp)
}

В этом примере создания функции:

  • выражение my_fun <- function(arg1, arg2) — это создание объекта-функции под названием my_fun;
  • arg1 и arg2 — два аргумента функции (функция может принять два разных значения и произвести над ними какие-то операции);
  • код в фигурных скобках — собственно тело функции, набор операций, которые должны совершаться над переданными значениями;
  • аргументы arg1 и arg2, объект tmp составляют рабочее окружение функции, оператор * и функция return() не входят в это окружение и используются как принадлежащие родительскому глобальному окружению (подробнее про окружения см. в Окружение функции);
  • return(tmp) - результат выполнения функции, который будет передан в глобальное окружение.

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

Создание функции из списка

Поскольку функции, так же, как и большинство объектов в R, могут быть представлены в виде списка, где первые элементы — аргументы функции, последний — тело функции или, в случае примитивных функций, выражение UseMethod('primitive_name'). Подобный список можно преобразовать в функцию с помощью as.function(), в первом элементе списка задать название аргумента (с классом symbol), вторым элементом — тело функции (в виде распарсенного, но не выполненного выражения):

# представляем объявленную ранее функцию в виде списка
as.list(my_fun)
## $arg1
## 
## 
## $arg2
## 
## 
## [[3]]
## {
##     tmp <- arg1 * arg2
##     return(tmp)
## }
# примитивная функция в виде списка
as.list(mean)
## $x
## 
## 
## $...
## 
## 
## [[3]]
## UseMethod("mean")
# задаём функцию в виде списка
my_fun_list <- list(
  # первый элемент — название аргумента с классом symbol
   x = as.symbol('x'),
   # второй аргумент — запись выражения
   quote(x ^ 2)
)
class(my_fun_list)
## [1] "list"
# преобразовываем в функцию и вызываем
my_fun_list <- as.function(my_fun_list)
class(my_fun_list)
## [1] "function"
my_fun_list(3)
## [1] 9

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

Аргументы функции

Аргументы функции указываются в круглых скобках при определении функции. В теле функции имена аргументов служат своего рода абстрактными названиями для любых объектов, которые переданы в аргументы при вызове функции. Собственно, передать какое-то значение в аргумент функции означает, что при выполнении функции над этим объектом будут проведены те операции, которые в коде (теле) функции проводятся над этим аргументом. В принципе, выражение “передать значение в аргумент” тождественно “использовать значение в качестве аргумента”, второе, возможно, даже более корректно.

Простейший пример функции с одним аргументом. Функция вычисляет квадратный корень и округляет результат до второго знака:

# создаём функцию
my_fun <- function(x) {
  z <- sqrt(x)
  z <- round(z, 2)
  z
}

# используем функцию
my_fun(5)
## [1] 2.24

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

# создаём объект в глобальном окружении
x <- 7

# создаём функцию с аргументом х
my_fun <- function(x) {
  # в теле функции создаём объект x из значения, переданного в аргумент функции
  x <- sqrt(x)
  x <- round(x, 2)
  x
}

# используем функцию, где объект x выступает в качестве аргумента
my_fun(x)
## [1] 2.65

Несмотря на то, что, как правило, все функции имеют свой набор аргументов, в редких случаях возможно создание функций вообще без аргументов. В таких случаях функции либо вычисляют и возвращают какое-то определённое значение, либо производят какие-то операции с объектами родительского окружения. Оба эти варианта, следует уточнить, не рекомендуется использовать, так как они либо бессмысленны и усложняют код, либо просто вредны и некорректны с точки зрения R. Редкими примерами осмысленного использования функций без аргументов могут послужить функции getwd(), Sys.time() и им подобные.

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

# создаём функцию
my_fun <- function() {
  x <- sqrt(5)
  x <- round(x, 2)
  x
}

# используем функцию
my_fun()
## [1] 2.24

Значения аргументов по умолчанию

Нередко в практике встречаются ситуации, когда один из аргументов функции принимает какое-то определённое значение (или значение из определённого вектора значений) намного чаще, чем все прочие возможные значения. В таких случаях разумно задать значение этому аргументу по умолчанию — то есть, если не указано обратное, будет использоваться заданное значение. Например, функция sort() имеет значение аргумента decreasing, по умолчанию равное FALSE. Соответственно, если не задавать этот аргумент, то функция сортирует вектор по возрастанию. И наоборот, если нужна сортировка по убыванию, следует прямо задать значение аргумента decreasing = TRUE:

sort(1:5)
## [1] 1 2 3 4 5
sort(1:5, decreasing = TRUE)
## [1] 5 4 3 2 1

Если посмотреть в справке описание аргументов функции sort(), то видно, что аргументу x никакое значение не передаётся, а аргументу decreasing - передаётся значение FALSE.

args(sort)
## function (x, decreasing = FALSE, ...) 
## NULL

Собственно, таким образом и задаются значения по умолчанию: при объявлении функции аргументу уже передаётся какое-то значение. Например, функция ниже умножает значение, переданное в качестве первого аргумента, на 2, если значение второго аргумента не указано:

# объявляем функцию my_fun, которая перемножает два переданных объекта
# если второй аргумент не указан, то считаем, что он равен 2
my_fun_def <- function(arg1, arg2 = 2) {
  tmp <- arg1 * arg2
  return(tmp)
}

Используем созданную функцию и в аргумент, у которого есть значение по умолчанию, ничего не передаём (игнорируем его):

# не указываем аргумент
x <- 9
my_fun_def(x)
## [1] 18

Точно так же при создании функции можно ограничить набор возможных значений, которые могут быть использованы в качестве какого-то аргумента. Как правило, это касается ситуаций, когда такой аргумент определяет метод вычисления и в теле функции организовано ветвление в зависимости от того, какое значение будет указано для этого аргумента. Ниже в примере для аргумента st мы задаём всего два значения, которые указывают, возвести значение аргумента x в степень y или вычислить корень соответствующей степени (возвести в 1/y степень). Если в качестве аргумента st используется другое значение, то функция вернёт предупреждение:

# создаём функцию с тремя аргументами
my_fun <- function(x, y, st = c('pow', 'root')) {
  if (st == 'pow') 
    return(x ^ y)
  if (st == 'root') 
    return(x ^ 1/y)
  message('unknown value for st')
}

# проверяем работу функции
my_fun(4, 2, st = 'pow')
## [1] 16
my_fun(4, 2, st = 'root')
## [1] 2
my_fun(4, 2, st = 'rt')
## unknown value for st

Точки (…)

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

# функция, которая выводит класс всех переданных аргументов
my_fun_dot <- function(...) sapply(list(...), class)
my_fun_dot('a', 5, TRUE)
## [1] "character" "numeric"   "logical"

В примере выше для того, чтобы предусмотреть ситуацию, что может быть передан не один аргумент, мы в теле функции собрали все переданные аргументы в список и уже к списку поэлементно применили функцию class(). Другим примером функций с подобным использованием трёх точек служат функции rbind() и cbind() — три точки на месте первого аргумента позволяет объединять с помощью этих функций построчно или по колонкам любое количество объектов:

args(rbind)
## function (..., deparse.level = 1) 
## NULL
args(cbind)
## function (..., deparse.level = 1) 
## NULL

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

# объявляем функцию
my_fun_dot <- function(x, y, ..., pwr) {
  z1 <- x * y
  z2 <- sapply(list(...), class)
  z3 <- pwr ^ 2
  list(z1, z2, z3)
}

# вызываем функцию с переданными аргументами
my_fun_dot(3, 7, 'a', TRUE, 3, pwr = 3)
## [[1]]
## [1] 21
## 
## [[2]]
## [1] "character" "logical"   "numeric"  
## 
## [[3]]
## [1] 9

В примере выше все аргументы — обязательные, необязательные и точки используются в результате вывода. Следовательно, если какой-то из аргументов не передать, то и вызов функции вернёт ошибку (правда, если не передать аргументы вместо точек, то вторым элементом в списке-результате будет пустой лист). Приведём простой пример ситуации, когда аргументы, переданные вместо точек, никак не используются в выводе:

my_fun_dot <- function(x, ...) sqrt(x)
my_fun_dot(9, 19)
## [1] 3

Последовательность использования аргументов

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

# создаём функцию
lazy_args <- function(arg1, arg2) {
  # в которой второй аргумент вычисляется по значению первого
  arg2 <- arg1 ^ 2
  # суммируем оба аргумента
  z <- arg1 + arg2
  return(z)
}

Вот так выглядит использование функции, в которой один из аргументов не определён на момент вызова функции и вычисляется в процессе выполнения:

lazy_args(arg1 = 3)
## [1] 12

Тело и результат функции

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

# объявляем функции
my_fun1 <- function(x) {x ^ 5}
my_fun2 <- function(x) x ^ 5

# вызываем функции
my_fun1(5)
## [1] 3125
my_fun2(5)
## [1] 3125

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

Неполное указание значений аргументов

Выше я уже упоминал, что в функциях в качестве значений аргумента по умолчанию может задаваться вектор определённых значений. Хорошим примером здесь будет функция вычисления расстояний dist(), в которой аргумент method может принимать одно из возможных значений (“euclidean”, “maximum”, “manhattan”, “canberra”, “binary” или “minkowski”). Каждое из этих значений можно указать как полным названием, так и первыми буквами названия, обеспечивающими уникальное толкование и выбор единственного значения. Например, выражение method = 'euc' так же работает, как и method = 'euclidean':

# задаём две точки с координатами (1, 1) и (2, 2)
x <- matrix(c(1, 2, 1, 2), ncol = 2, nrow = 2)

# вычисляем расстояние между точками
dist(x, method = 'euc')
##          1
## 2 1.414214
dist(x, method = 'euclidean')
##          1
## 2 1.414214

Подобное поведение реализовано за счёт использования функции match.arg() в теле вызываемой функции. В функцию match.agr() передаётся значение какого-то аргумента функции, вторым аргументом — вектор его возможных значений (если его нет, берётся тот, который указан в значениях аргумента по умолчанию). Ниже пример из справки по функции match.arg(), где создаёется функция оценки центральной тенденции и предполагается три разных метода — среднее, медиана и усечённое среднее:

center <- function(x, type = c("mean", "median", "trimmed")) {
  # верифицируем аргумент в случае неполной записи
  type <- match.arg(type)
  
  # в зависимости от значения type вызываем разную оценку центральной тенденции
  switch(type,
         mean = mean(x),
         median = median(x),
         trimmed = mean(x, trim = .1))
}

# применяем функцию с точным и неполным значением аргумента type
x <- rnorm(100)
center(x, type = 'trimmed')
## [1] -0.05891615
center(x, type = 'tri')
## [1] -0.05891615

Функция match.arg() также позволяет задавать ситуации, когда в аргумент может быть передано одновременно больше чем одно значение из вектора возможных. Однако это осмысленно только в том случае, когда код функции сам по себе предполагает возможность двух или более значений аргумента. Например:

center_names <- function(x, type = c("mean", "median", "trimmed")) {
  # верифицируем аргумент в случае неполной записи, допускаем несколько вариантов
  type <- match.arg(type, several.ok = TRUE)
  
  # пробегаем по вектору переданных значений типа центр. тенденции, вычисляем
  res <- sapply(type, function(z) {
    res <- ifelse(z == 'trimmed', mean(x, trim = .1), do.call(z, list(x)))
  })
  
  # возвращаем результат
  res
}

# вычисляем среднее и усечённое среднее
my_data <- rnorm(100)
center_names(my_data, type = c('mea', 'tr'))
##         mean      trimmed 
## -0.007724408 -0.022402681
center_names(my_data, type = c('mean', 'trimmed'))
##         mean      trimmed 
## -0.007724408 -0.022402681

Проверка классов используемых значений

Достаточно часто встречаются ситуации, когда в качестве аргументов функций используются объекты иных классов, чем предполагается в коде функции. Например, это может быть ошибка импорта данных, когда числа импортируются как строковые значения или даты в источнике данных представлены не в ISO-формате (как правило, такое случается при импорте из xlsx с настройками по умолчанию). Точно так же случаются ситуации, когда на каком-то из предыдущих этапов процессинга была получена ошибка и один из используемых в функции объектов является NULL-объектом. Все эти случаи требуют включения в тело функции проверки существования и класса используемых значений. В примере ниже делается проверка, что переданный объект — числовой. В рамках базового R подобные проверки можно делать с помощью конструкций if...else или же использовать специализированные пакеты (про return() и invisible() ниже).

# объявляем функцию, которая делает дополнительно проверку типа
my_fun <- function(x) {
  if(!is.numeric(x)) {
    message('Alarm! Not a real number!')
    return(invisible(NULL))
  }
  sqrt(x)
}
# проверяем, как работает
my_fun(5)
## [1] 2.236068
my_fun('char')
## Alarm! Not a real number!

Возвращаемый результат функции

Большинство функций в результате своей работы возвращает один объект. Объектом может быть вектор значений, список, таблица, другая функция и так далее. Для того чтобы указать, какой именно объект должна вернуть функция, используется return() и, что важно, использовать эту функцию можно в любом месте тела функции. Впрочем, возможен и более лаконичный вариант, когда самой последней строчкой тела функции указывается название возвращаемого объекта. Следует учитывать, что это должно быть именно имя объекта или какое-то выражение, создающее новый объект (*pply-функции, function(), data.frame() и так далее), за исключением операции присвоения:

# используем return(x) в середине кода
my_fun1 <- function(x) {
  x <- x ^ 3
  return(x)
  x <- x * 2
}

# возвращаем x просто последней строчкой
my_fun2 <- function(x) {
  x <- x ^ 3
  x
}

# проверяем
my_fun1(2)
## [1] 8
my_fun2(2)
## [1] 8

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

# подавляем вывод результата
my_fun <- function(x) {
  x <- x ^ 2
  invisible(x)
}

# вызываем функцию
my_fun(3)

# вызываем функцию с форсированным выводом результата
(my_fun(3))
## [1] 9
# сравниваем скрытый результат функции с реальным значением
my_fun(3) == 3 ^ 2
## [1] TRUE

Функции высших порядков

Функции высших порядков — функции, в аргументы которых можно передавать другие функции. Классическим примером подобных функций в R являются функции *pply-семейства, например, sapply():

sapply(1:5, sqrt)
## [1] 1.000000 1.414214 1.732051 2.000000 2.236068

Создание подобных функций ничем не отличается от простых функций, и весь процесс основан на идее, что в R названия функции — это такие же пары “название объекта + его значение”. Это позволяет прямо передавать название функции в качестве аргумента. Например, создадим аналог функции sapply(), которая возвращает вектор значений вектора x после того, как к ним была применена функция, переданная в аргумент new_f:

myapply <- function(x, new_f) {
  result <- x
  for (i in seq_along(x)) 
    result[i] <- new_f(x[i])
  result
}

myapply(1:5, sqrt)
## [1] 1.000000 1.414214 1.732051 2.000000 2.236068
myapply(1:5, function(x) x^2)
## [1]  1  4  9 16 25

Специальные действия при завершении функции

Функция on.exit() указывает, какое выражение должно быть выполнено при завершении работы функции. Как правило, это полезно в тех случаях, когда в процессе выполнения функции изменяются какие-то глобальные настройки — параметры сессии, адрес рабочей директории, графические настройки и так далее. Второе применение функции on.exit() — завершение работы функции, даже если в процессе выполнения функции произошла ошибка (недоступна база данных, ошибка в выражении, нет данных в датасете и проч.). Например, функция, которая меняет рабочую директорию, возводит переданное значение в квадрат, а на выходе возвращает исходное значение рабочей директории:

my_fun_exit <- function(x) {
  # сохраняем и печатаем адрес текущей рабочей директории
  old_dir <- getwd()
  print(old_dir)
  
  # создаём папку и устанавливаем новую директорию — домашнюю папку
  dir.create('./new_dir', showWarnings = FALSE)
  setwd('./new_dir')
  
  # проверяем, что рабочая директория поменялась
  print(getwd())
  
  # задаём, что на выходе надо вернуться к старой рабочей директории
  on.exit({
    setwd(old_dir)
    
    # удаляем новую директорию
    unlink('new_dir', recursive = TRUE)
  })
  
  
  # возводим переданный аргумент в квадрат и возвращаем его
  z <- x ^ 2
  z
}
  
# применяем функцию и смотрим текущую рабочую директорию
my_fun_exit(5)
## [1] "/home/konhis/Documents/Rprojects/r_textbook"
## [1] "/home/konhis/Documents/Rprojects/r_textbook/new_dir"
## [1] 25
## [1] "/home/konhis/Documents/Rprojects/r_textbook"

Обработка ошибок и предупреждений

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

Основной обработчик ошибок — функция tryCatch(). Функция tryCatch() имеет три группы аргументов:

  • собственно тестируемое выражение (аргумент expr);
  • обработчики событий (..., вместо которых можно использовать error, warning, message);
  • выражение, результат которого будет возвращён в результате работы функции (finally).

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

# задаём функцию с tryCatch
my_fun <- function(x) {
  tryCatch(
    # задаём выражение
    expr = {
      sqrt(x)
      library(noname_package)
    },
    # указываем, что делать, если появится ошибка
    error = function(e) print(paste('получили ошибку:', e)),
    # указываем, что делать, если появится предупреждение
    warning = function(w) print(paste('получили предупреждение:', w)),
    # указываем, что делать, если появится сообщение
    message = function(m) print(paste('получили сообщение:', m)),
    # указываем, что делать при завершении функции
    finally = print('finally: this is the end, my friend')
  )
}
# используем такое значение аргумента, чтобы получить предупреждение
my_fun(-5)
## [1] "получили предупреждение: simpleWarning in sqrt(x): NaNs produced\n"
## [1] "finally: this is the end, my friend"
# используем такое значение аргумента, чтобы получить ошибку
my_fun(5)
## [1] "получили ошибку: Error in library(noname_package): there is no package called 'noname_package'\n"
## [1] "finally: this is the end, my friend"

Такое поведение возможно за счёт того, что при выполнении выражения возможные ошибки или прочие события пишутся в соединение stderr() каким-то из простых классов ряда simpleWarning, simpleError или simpleMessage. Соответственно, выражение вида error = function(e) print(paste('получили ошибку:', e)) как раз отслеживает появление ошибки во время выполнения выражения и возвращает определённое действие. В нашем случае это создание и печать текстовой строки из 'получили ошибку:' и текста ошибки. Аргумент e является лишь указателем на ошибку и может быть назван как угодно в пределах разрешённых слов.

Следует учитывать, что в данном случае порядок имеет значение и tryCatch() вернёт то действие, которое встретится первым. В примере выше предупреждение при выполнении выражения sqrt(-5) получено раньше однозначно ошибочного выражения library(noname_package). Поэтому в результате выполнения tryCatch() мы получили результат выражения, которое задано для предупреждений, и результат выражения в finally. При этом выражение, заданное для ошибок (function(e) print(paste('получили ошибку:', e))), проигнорировано и не выполнено.

Инфиксные функции (операторы)

Объявленные функции, которые присутствуют в окружении в виде самостоятельных объектов, можно разделить на префиксные, инфиксные функции и модифицирующие функции. При использовании префиксных функций название функции предваряет список аргументов, например, my_fun(x, y), это основная форма использования функций в R. Инфиксные функции вызываются между двумя объектами, над которыми происходит действие. Фактически все используемые операторы в R — это инфиксные функции. При желании инфиксные функции можно вызвать в виде префиксных, в таких случаях название функции выделяется обратными апострофами:

# обычный инфиксный вид оператора сложения
3 + 4
## [1] 7
# представление в постфиксной записи
`+`(3, 4)
## [1] 7

Как правило, инфиксные функции носят вид %fun_name% — то есть название функции заключено в знаки процента (%). Исключение составляют только функции, переопределяющие операторы из зарезервированного набора бинарных операторов (+, - и проч.). Ниже пример оператора пересечений множеств — только три значения множества 1:5 входят в множество значений 2:4:

1:5 %in% 2:4
## [1] FALSE  TRUE  TRUE  TRUE FALSE

Так как операторы — это такие же функции, то можно создать свой оператор или даже переназначить уже существующий. Стоит учитывать, что изменение поведения привычных операторов крайне не приветствуется, поэтому лучше создавать свои собственные операторы на основе сочетаний букв. Для этого формируется название оператора (название функции и знаки процента слева и справа от него). Далее эта запись заключается в обратные апострофы из-за нестандартного для R именования объектов, и полученный результат используется как название создаваемой функции. В примере ниже создаётся функция, обратная %in%, — поиск значений одного множества, не входящих во второе множество. В данном случае для идентичности с функцией %in% выводится логический вектор, несмотря на то что корректнее было бы написать x[!(x %in% y)]:

`%nin%` <- function(x, y) !(x %in% y)
1:5 %nin% 2:4
## [1]  TRUE FALSE FALSE FALSE  TRUE

Функции модификации

Третий вид функций, наряду с префиксными и инфиксными функциями, — функции замещения. Такие функции используются, когда необходимо изменить объект или элемент объекта. Например, выражение names(my_df) <- c('col1', 'col2') как раз представляет собой использование функции names<-, так как одновременно вызываются названия колонок таблицы my_df и им присваиваются новые значения. Выражение x[2] <- 2 также представляет собой применение функции замещения [<-.

По форме эти функции аналогичны префиксным функциям, однако в названии, как видно из примера, содержат знак <-. При объявлении этих функций первым аргументом обычно указывают объект, над которым производится действие (x), вторым аргументом — номер замещаемого элемента, position (если релевантно), третьим аргументом — новое значение (value). В данном случае закрепленные и обязательные аргументы только x и value, элемент position может быть назван как угодно, к тому же подобных дополнительных аргументов может быть несколько. Так как название должно содержать <-, то оно заключается в обратные апострофы:

`my_fun_replacement<-` <- function(x, position, position2, value) {
  x[position] <- value
  x[position2] <- 'additonal value'
  x
}

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

# создаём вектор значений
my_vec <- c('a', 'b', 'c')

# используем стандартную форму, создаём четвертый элемент
`my_fun_replacement<-`(x = my_vec, position = 4, position2 = 2, value = 'forth element')
## [1] "a"               "additonal value" "c"               "forth element"
my_vec
## [1] "a" "b" "c"
# используем оператор присваивания
my_fun_replacement(my_vec, 4, 2) <- 'forth element'
my_vec
## [1] "a"               "additonal value" "c"               "forth element"

Анонимные функции

Помимо объявленных функций, существующих в виде самостоятельных объектов, в R можно использовать так называемые анонимные функции. Анонимные функции не требуют создания объекта функции и представляют собой просто набор операций, выполняемых с переданными в определённый момент в аргументы функции значениями. Наиболее часто анонимные функции используются вместе с *pply-функциями:

sapply(letters[1:5], function(x) paste("letter", x))
##          a          b          c          d          e 
## "letter a" "letter b" "letter c" "letter d" "letter e"

В примере выше выражение function(x) paste("letter", x) и есть анонимная функция с одним аргументом. Результат выполнения этой функции — склейка строки 'letter' и значения переданного аргумента. Аргументы в анонимную функцию передаются из первого аргумента функции sapply(), в данном случае это вектор из первых пяти букв латинского алфавита.

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

# создаём датасет и выводим его на печать
tmp <- data.frame(v1 = 'A', v2 = c('p1', 'p1', 'p2', 'p2'), v3 = 3:6)
print(tmp)
##   v1 v2 v3
## 1  A p1  3
## 2  A p1  4
## 3  A p2  5
## 4  A p2  6
# переводим таблицу из длинного формата в широкий с вычислением среднего по комбинации v1 ~ v2
reshape2::dcast(tmp, v1 ~ v2, value.var = 'v3', fun.aggregate = mean)
##   v1  p1  p2
## 1  A 3.5 5.5
# переводим таблицу из длинного формата в широкий с вычислением квадрата среднего по комбинации v1 ~ v2
reshape2::dcast(tmp, v1 ~ v2, value.var = 'v3', fun.aggregate = function(x) mean(x) ^ 2)
##   v1    p1    p2
## 1  A 12.25 30.25

Документирование функций

Писать комментарии к коду и вести сопроводительную документацию — необходимая практика, особенно когда проект достаточно большой и в нём участвуют несколько разработчиков. В R принято документировать функции, датасеты, S4-классы и методы, а также пакеты.

Чаще всего пользователи могут встретиться с документацией по функциям в пакетах (папка /man) — там она представлена в .Rd-файлах в особой разметке. В настоящее время .Rd-файлы документации создаются не вручную, а конвертируются из блока roxygen-комментариев, которые пишутся при объявлении функций.

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

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

Вызвать roxygen-скелетон (базовый набор тегов) в RStudio можно с помощью комбинации Cmd/Ctrl + Shift + D (зависит от OS и используемой комбинации клавиш для переключения языка) либо же написать вручную. Вот так выглядит заполненный блок комментариев (roxygen-комментарии начинаются с #'):

#' @title Пример документации функции
#' @description Функция выводит на печать строку 'Тестируем roxygen'
#' @param x Может быть любым, так как не используется.
#' @return NULL
#' @examples
#' my_fun(19)
#' @author Philipp Upravitelev <my@@email.com>
my_fun <- function(x) {
  print('Тестируем roxygen')
  return(invisible(NULL))
}
x <- my_fun(5)
## [1] "Тестируем roxygen"

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

Второй параграф дает короткий комментарий, что делает функция. Можно явно задать тегом @description. Первый и второй параграфы обязательны в документации, остальные можно опустить.

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

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

  • @param — используется для каждого аргумента, указывается его тип/класс, а также при необходимости возможные значения и их расшифровка, если в функции используется несколько аргументов одного типа/класс, их можно объединить;
  • @return — что возвращает функция, тип объекта, также описание возможных вариаций;
  • @examples — пример использования функции;
  • @author — автор и его e-mail (при желании).

В roxygen-комментариях можно использовать специальную .Rmd-разметку (например, \code{utils::str()}) или, с недавних пор, markdown.

roxygen2 не предполагает создания отдельных файлов документации. Поэтому, если хочется иметь файл документации по какой-то функции, можно воспользоваться пакетом document. Функция document::document() создаёт из roxygen-комментариев файл документации в трёх форматах (pdf, html, txt). К сожалению, кириллицу пакет воспринимает не очень хорошо, но это не мешает работе. В результате работы функция возвращает список путей, по которым расположены файлы документации (если не указывать output_directory, то они будут созданы как временные файлы).

docs_path <- my_doc <-
  document::document(
    file_name = './src/roxygen_example.R',
    check_package = FALSE,
    sanitize_Rd = TRUE,
    output_directory = './src/'
  )
## First time using roxygen2. Upgrading automatically...
## Updating roxygen version in /tmp/Rtmp3tj3TI/document_84400573f0cda/roxygen.example/DESCRIPTION
## Warning: roxygen2 requires Encoding: UTF-8
## ℹ Loading roxygen.example
## Warning in readLines(con, warn = FALSE, n = n, ok = ok, skipNul = skipNul):
## invalid input found on input connection '/tmp/Rtmp3tj3TI/document_84400573f0cda/
## roxygen.example/R/code.R'

Смотрим результат:

cat ./src/roxygen_example.txt
## Пример документации функции
## 
## Description:
## 
##      Функция выводит на печать строку 'Тестируем roxygen'
## 
## Usage:
## 
##      my_fun(x)
##      
## Arguments:
## 
##        x: Может быть любым, так как не используется.
## 
## Author(s):
## 
##      Philipp Upravitelev <my@email.com>
## 
## Examples:
## 
##      my_fun(19)
##