---
title: "Использование data.table в других пакетах"
date: "`r Sys.Date()`"
output:
  markdown::html_format
vignette: >
  %\VignetteIndexEntry{Использование data.table в других пакетах}
  %\VignetteEngine{knitr::knitr}
  \usepackage[utf8]{inputenc}
---

<style>
h2 {
    font-size: 20px;
}
</style>

Этот документ рассказывает об использовании `data.table` в качестве
зависимости в других R-пакетах. Если вас интересует использование C-кода
`data.table` в приложениях, не связанных с R, или прямой вызов его
C-функций, перейдите к [последнему разделу](#non-r-api) этого руководства.

Импортирование `data.table` не отличается от импортирования других пакетов
R. Этот раздел предназначен для ответа на самые распространенные вопросы по
этой теме; представленные здесь уроки могут быть применены и к другим
пакетам R.

## Зачем импортировать `data.table`

Одной из главных особенностей `data.table` является его лаконичный
синтаксис, который делает исследовательский анализ быстрее и легче как для
написания, так и для восприятия. Эта удобство может побудить авторов пакетов
использовать `data.table`. Ещё более важной причиной является высокая
производительность. Передавая ресурсоёмкие вычислительные задачи из вашего
пакета в `data.table`, вы получаете максимальную производительность, не
прибегая к созданию собственных методов числовой оптимизации.

## Импортировать `data.table` просто

Использовать `data.table` в качестве зависимости очень просто, так как у
`data.table` нет собственных зависимостей. Это касается как операционной
системы, так и зависимостей R. Если у вас установлена R, то на вашем
устройстве уже есть всё необходимое для установки `data.table`. Это также
означает, что добавление `data.table` в качестве зависимости для вашего
пакета не вызовет цепочку других рекурсивных зависимостей, что делает
установку удобной даже в режиме офлайн.

## Файл `DESCRIPTION` {#DESCRIPTION}

Первое место для определения зависимостей в пакете - это файл `DESCRIPTION`. Чаще всего вам нужно добавить `data.table` в поле `Imports:`. Это потребует установки `data.table` перед компиляцией/установкой вашего пакета. Как упоминалось выше, никакие другие пакеты устанавливаться не будут, поскольку `data.table` не имеет собственных зависимостей. Вы также можете указать минимально необходимую версию зависимости; например, если ваш пакет использует функцию `fwrite`, которая появилась в `data.table` в версии 1.9.8, вы должны включить это как `Imports: data.table (>= 1.9.8)`. Таким образом, вы сможете убедиться, что установленная версия `data.table` - 1.9.8 или более поздняя, прежде чем ваши пользователи смогут установить ваш пакет. Помимо поля `Imports:`, вы также можете использовать `Depends: data.table`, но мы настоятельно не рекомендуем использовать этот подход (и, возможно, запретим его в будущем), поскольку он загружает `data.table` в рабочее пространство вашего пользователя; то есть, он включает функциональность `data.table` в скрипты ваших пользователей без их запроса. `Imports:` - это правильный способ использовать `data.table` в вашем пакете, не навязывая `data.table` его пользователю. Мы надеемся, что поле `Depends:` в R со временем будет упразднено, поскольку это справедливо для всех пакетов.

## Файл `NAMESPACE` {#NAMESPACE}

Следующий шаг - это определить, какие функции из `data.table` использует ваш
пакет. Это нужно сделать в файле `NAMESPACE`. Наиболее часто авторы пакетов
используют `import(data.table)`, что позволит импортировать все
экспортируемые (то есть перечисленные в собственном файле `NAMESPACE` пакета
`data.table`) функции из `data.table`.

Возможно, вы захотите использовать только часть функций `data.table`;
например, некоторые пакеты могут использовать только высокопроизводительные
функции чтения и записи CSV из `data.table`. В этом случае вы можете
добавить `importFrom(data.table, fread, fwrite)` в ваш файл
`NAMESPACE`. Также возможно импортировать все функции из пакета, _за
исключением_ определенных, используя `import(data.table, except=c(fread,
fwrite))`.

Не забудьте также ознакомиться с примечанием о нестандартных вычислениях в
`data.table` в разделе [«Неопределенные глобальные переменные»](#globals)

## Использование

В качестве примера мы зададим две функции в пакете `a.pkg`, который
использует `data.table`. Одна функция, `gen`, будет генерировать простой
`data.table`; другая, `aggr`, будет выполнять простую агрегацию этой
`data.table`.

```r
gen = function (n = 100L) {
  dt = as.data.table(list(id = seq_len(n)))
  dt[, grp := ((id - 1) %% 26) + 1
     ][, grp := letters[grp]
       ][]
}
aggr = function (x) {
  stopifnot(
    is.data.table(x),
    "grp" %in% names(x)
  )
  x[, .N, by = grp]
}
```

## Тестирование

Обязательно включите тесты в ваш пакет. Перед каждым крупным релизом
`data.table` мы проверяем опубликованные пакеты, которые зависят от нас. Это
означает, что если изменения в `data.table` нарушат ваш код, мы сможем
обнаружить эти проблемы и уведомить вас до выпуска новой версии. Это
предполагает, что вы опубликуете ваш пакет в CRAN или Bioconductor. Самый
базовый тест может быть представлен в виде простого R-скрипта в каталоге
вашего пакета `tests/test.R`:

```r
library(a.pkg)
dt = gen()
stopifnot(nrow(dt) == 100)
dt2 = aggr(dt)
stopifnot(nrow(dt2) < 100)
```

При тестировании вашего пакета вы можете использовать команду `R CMD check
--no-stop-on-test-error`, которая продолжит выполнение после ошибки и
запустит все ваши тесты (в отличие от остановки на первой строке скрипта с
ошибкой). Имейте в виду, что это требует R версии 3.4.0 или выше.

## Тестирование при помощи `testthat`

Очень часто для тестирования используется пакет `testthat`. Тестирование
пакета, который импортирует `data.table`, не отличается от тестирования
других пакетов. Пример тестового скрипта `tests/testthat/test-pkg.R`:

```r
context("pkg tests")

test_that("generate dt", { expect_true(nrow(gen()) == 100) })
test_that("aggregate dt", { expect_true(nrow(aggr(gen())) < 100) })
```

Если `data.table` находится в разделе Suggests (но не в Imports), то вам
нужно написать `.datatable.aware=TRUE` в одном из файлов R/*, чтобы избежать
ошибок "объект не найден" при тестировании с помощью
`testthat::test_package` или `testthat::test_check`.

## Работа с "неопределёнными глобальными функциями или переменными" {#globals}

Пока что `R CMD check` может выдавать предупреждения при виде нестандартных
вычислений R, используемых при работе с `data.table` (особенно с левой
стороны `:=`) . Во время проверки пакета это приводит к сообщениям `NOTE`
следующего типа:

```
* checking R code for possible problems ... NOTE
aggr: no visible binding for global variable 'grp'
gen: no visible binding for global variable 'grp'
gen: no visible binding for global variable 'id'
Undefined global functions or variables:
grp id
```

Самый простой способ справиться с этим — предварительно определить эти
переменные в вашем пакете и установить их как `NULL`, при необходимости
добавив комментарий (как это сделано в уточнённой версии функции `gen`
ниже). При возможности также можно использовать вектор символов вместо
символов (как в функции `aggr` ниже):

```r
gen = function (n = 100L) {
  id = grp = NULL # избегаем предупреждений от R CMD check
  dt = as.data.table(list(id = seq_len(n)))
  dt[, grp := ((id - 1) %% 26) + 1
     ][, grp := letters[grp]
       ][]
}
aggr = function (x) {
  stopifnot(
    is.data.table(x),
    "grp" %in% names(x)
  )
  x[, .N, by = "grp"]
}
```

Случай со специальными символами `data.table` (например, `.SD` и `.N`) и
оператором присвоения (`:=`) немного отличается (см. `?.N` для получения
дополнительной информации, включая полный список таких символов). Вам
следует импортировать те значения, которые вы используете, из пространства
имён `data.table`, чтобы защититься от возможных проблем, если в будущем мы
изменим экспортируемое значение этих символов, например, если вы хотите
использовать `.N`, `.I` и `:=`, минимальный `NAMESPACE` будет содержать:

```r
importFrom(data.table, .N, .I, ':=')
```

Гораздо проще использовать `import(data.table)`, что позволит коду вашего
пакета использовать любой объект, экспортируемый из `data.table`.

Если вам не мешает, что `id` и `grp` будут зарегистрированы как глобальные
переменные в пространстве имен вашего пакета, вы можете использовать
`?globalVariables`. Учтите, что эти примечания не влияют на код или его
функциональность; если вы не собираетесь публиковать свой пакет, вы можете
их проигнорировать.

## При работе с `options()` требуется осторожность

Пакеты R могут использовать систему опций, которые пользователь задаёт при
помощи `options(имя=значение)`. Чтобы узнать значение опции, используют
`getOption("имя", значение_по_умолчанию)`. Аргументы функций по-умолчанию
часто включат вызов `getOption()`, чтобы пользователь знал (из `?fun` или
`args(fun)`), какое имя параметра управляет значением по умолчанию для этого
параметра; например, `fun(..., verbose=getOption("datatable.verbose",
FALSE))`. Все опции `data.table` начинаются с `datatable.`, чтобы не
конфликтовать с параметрами других пакетов. Пользователь просто вызывает
`options(datatable.verbose=TRUE)`, чтобы включить этот режим. Это влияет на
все вызовы функций `data.table`, если явно не указан `verbose=FALSE`;
например, `fun(..., verbose=FALSE)`.

Механизм опций в R является _глобальным_. Это означает, что если
пользователь устанавливает опцию `data.table` для собственного пользования,
то эта установка также влияет на код в любом пакете, использующем
`data.table`. Для такой опции, как `datatable.verbose`, это желаемое
поведение, поскольку целью является отслеживание и протоколирование всех
операций `data.table`, откуда бы они ни исходили; включение этой опции не
влияет на результаты. Еще одна уникальная для R и полезная на практике опция
— это `options(warn=2)`, которая превращает все предупреждения в ошибки. Её
цель состоит в том, чтобы не пропустить ни одного предупреждения при
практическом использовании кода. Есть 6 опций `datable.print.*` и 3 опции
оптимизации, которые не влияют на результат операций. Однако есть одна опция
`data.table`, которая на него влияет и в данный момент вызывает опасения:
`datatable.nomatch`. Эта опция изменяет выполняемое по-умолчанию соединение
с внешнего на внутреннее. [Внешнее соединение используется по умолчанию,
потому что оно не отбрасывает отсутствующие данные и поэтому считается
безопаснее; кроме того, оно соответствует тому, как имена и индексы работают
в самом R.] Некоторые пользователи предпочитают, чтобы по умолчанию
использовалось внутреннее соединение, и мы предусмотрели для них эту
опцию. Однако пользователь, установивший эту опцию, может непреднамеренно
изменить поведение соединений внутри пакетов, использующих
`data.table`. Соответственно, в версии 1.12.4 (октябрь 2019) при
использовании опции `datatable.nomatch` выводилось сообщение, а начиная с
версии 1.14.2 она теперь игнорируется с предупреждением. Это была
единственная опция `data.table`, вызывавшая подобные опасения.

## Устранение неполадок

Если вы столкнулись с проблемами при создании пакета, использующего
`data.table`, убедитесь, что проблема воспроизводится в новой сессии R,
используя консоль R: `R CMD check package.name`.

Некоторые из наиболее распространенных проблем, с которыми сталкиваются
разработчики, обычно связаны с вспомогательными инструментами,
предназначенными для автоматизации некоторых задач разработки пакета,
например, использование `roxygen` для генерации файла `NAMESPACE` из
метаданных в файлах кода R. Другие связаны с вспомогательными функциями,
которые собирают и проверяют пакет. К сожалению, они иногда непредумышленно
приводят к побочным эффектам, которые могут скрыть источник ваших
проблем. Поэтому перепроверьте наличие импорта в файлах `DESCRIPTION` и
`NAMESPACE` в соответствии с [инструкциями](#DESCRIPTION)
[выше](#NAMESPACE).

Если Вы не можете воспроизвести возникшие проблемы при сборке и проверке при
помощи R в командной строке, Вы можете попробовать обратиться за поддержкой,
основываясь на прошлых проблемах, с которыми мы сталкивались при
взаимодействии `data.table` с вспомогательными инструментами:
[devtools#192](https://github.com/r-lib/devtools/issues/192) или
[devtools#1472](https://github.com/r-lib/devtools/issues/1472).

## Лицензия

Начиная с версии 1.10.5, `data.table` распространяется на условиях Mozilla
Public License (MPL). С причинами перехода с GPL можно ознакомиться
[здесь](https://github.com/Rdatatable/data.table/pull/2456), а подробнее о
MPL Вы можете прочитать в Википедии
[здесь](https://en.wikipedia.org/wiki/Mozilla_Public_License) и
[здесь](https://en.wikipedia.org/wiki/Comparison_of_free_and_open-source_software_licenses).

## Необязательный импорт `data.table`: Suggests

Если Вы хотите использовать `data.table` необязательным образом, то есть
только тогда, когда он установлен, вам следует использовать `Suggests:
data.table` в вашем файле `DESCRIPTION` вместо `Imports: data.table`. По
умолчанию, `data.table` не будет установлена при установке Вашего
пакета. Это также потребует от Вас добавления проверок на наличие
`data.table` в код Вашего пакета, что можно сделать с помощью функции
`?requireNamespace`. Приведенный ниже пример демонстрирует условное
использование быстрой записи CSV из `data.table` при помощи функции
`?fwrite`. Если пакет `data.table` не установлен, вместо него используется
гораздо более медленная базовая функция R `?write.table`.

```r
my.write = function (x) {
  if(requireNamespace("data.table", quietly=TRUE)) {
    data.table::fwrite(x, "data.csv")
  } else {
    write.table(x, "data.csv")
  }
}
```

Несколько более расширенная версия этого способа также гарантирует, что
установленная версия `data.table` является достаточно свежей, чтобы в ней
была доступна функция `fwrite`:

```r
my.write = function (x) {
  if(requireNamespace("data.table", quietly=TRUE) &&
    utils::packageVersion("data.table") >= "1.9.8") {
    data.table::fwrite(x, "data.csv")
  } else {
    write.table(x, "data.csv")
  }
}
```

При использовании пакета в качестве предполагаемой зависимости не следует
использовать `import` в файле `NAMESPACE`. Достаточно упомянуть его в файле
`DESCRIPTION`.  При использовании функций `data.table` в коде пакета (файлы
`R/*`) необходимо использовать префикс `data.table::`, поскольку ни одна из
них не импортируется.  При использовании `data.table` в тестах пакета
(например, в файлах `tests/testthat/test*`) необходимо создать переменную
`.datatable.aware=TRUE` в одном из файлов `R/*`.

## `data.table` в `Imports`, но функции явно не импортированы

Некоторые пользователи
([пример](https://github.com/Rdatatable/data.table/issues/2341)) могут
предпочесть отказаться от использования `importFrom` или `import` в файле
`NAMESPACE` и вместо этого использовать полное имя `data.table::*` в своём
коде (разумеется, сохраняя `data.table` в секции `Imports:` файла
`DESCRIPTION`).

В этом случае неэкспортированная функция `[.data.table` превратится в вызов
`[.data.frame` в качестве меры предосторожности, поскольку `data.table` не
имеет возможности узнать, что родительский пакет осведомлен о том, что он
пытается выполнить вызов синтаксиса API запросов `data.table` (что может
привести к неожиданному поведению, поскольку структура вызовов
`[.data.frame` и `[.data.table` принципиально отличается: например,
последний имеет гораздо больше аргументов).

Если Вы предпочитаете такой подход к разработке пакетов, задайте переменную
`.datatable.aware = TRUE` в любом месте исходного кода R (экспортировать её
не нужно). Это сообщит `data.table`, что Вы, как разработчик пакета,
спроектировали свой код так, чтобы намеренно полагаться на функциональность
`data.table`, даже если это может быть не очевидно при просмотре вашего
файла `NAMESPACE`.

`data.table` на лету определяет, знает ли вызывающая функция, что она
обращается к `data.table`, с помощью внутренней функции `cedta`
(«**C**alling **E**nvironment is **D**ata **T**able **A**ware», окружение
вызова функции знает про `data.table`), которая, помимо проверки
`?getNamespaceImports` для вашего пакета, также проверяет существование этой
переменной (и некоторые другие вещи).

## Дополнительная информация о зависимостях

Более официальную документацию о зависимостях пакетов и способах их
объявления можно найти в официальном руководстве: [Writing R
Extensions](https://cran.r-project.org/doc/manuals/r-release/R-exts.html).

## Импорт функций на C из data.table

Некоторые из внутренне используемых подпрограмм на C теперь экспортированы
для другого кода на C и могут быть использованы в пакетах R непосредственно
из кода на C. Подробнее о том, как это делать, см. в
[`?cdt`](https://rdatatable.gitlab.io/data.table/reference/cdt.html) и в
разделе [Writing R
Extensions](https://cran.r-project.org/doc/manuals/r-release/R-exts.html)
_Linking to native routines in other packages_.

## Импорт извне R {#non-r-api}

Некоторые небольшие части Си-кода `data.table` были изолированы от R C API и
теперь могут быть использованы из приложений, не относящихся к R, путем
компоновки с файлами .so / .dll. Более подробная информация об этом будет
предоставлена позже, а пока вы можете изучить Си-код, который был изолирован
от R C API в
[src/fread.c](https://github.com/Rdatatable/data.table/blob/master/src/fread.c)
и
[src/fwrite.c](https://github.com/Rdatatable/data.table/blob/master/src/fwrite.c).

## Как преобразовать зависимость от data.table из Depends в Imports

Чтобы преобразовать зависимость Вашего пакета от `data.table` типа `Depends`
в зависимость типа `Imports`, выполните следующие действия:

### Шаг 0. Убедитесь, что ваш пакет изначально проходит R CMD check

### Шаг 1. Обновите файл DESCRIPTION, переместив data.table из Depends в Imports

**До:**
```dcf
Depends:
    R (>= 3.5.0),
    data.table
Imports:
```

**После:**
```dcf
Depends:
    R (>= 3.5.0)
Imports:
    data.table
```

### Шаг 2.1: Выполните команду `R CMD check`

Запустите `R CMD check`, чтобы выявить недостающие импорты. Этот шаг:

- Автоматически обнаруживает любые функции или символов из `data.table`,
  которые не были импортированы явно.
- Отмечает отсутствующие специальные символы, такие как `.N`, `.SD` и `:=`.
- Сразу же пишет, что нужно добавить в файл NAMESPACE.

Замечание: `R CMD check` ловит не все подобные случаи использования. В
частности, `R CMD check` пропускает некоторые символы/функции в формулах и
полностью пропустит собранные из текста выражения типа `parse(text =
"data.table(a = 1)")`. Для обнаружения таких краевых случаев пакетам
потребуется хорошее покрытие тестами.

### Шаг 2.2: Измените файл NAMESPACE

Основываясь на результатах `R CMD check`, импортируйте из `data.table` все
необходимые функции, специальные символы, общие функции S3 и классы S4.

Это означает добавление директив `importFrom(data.table, ...)` для символов,
функций и S3-генериков, а при необходимости также директив
`importClassesFrom(data.table, ...)` для S4-классов. Подробности о том, как
правильно это сделать, см. в руководстве "Writing R Extensions".

#### Полный импорт

В качестве альтернативы вы можете импортировать всё, что экспортирует
`data.table`, хотя обычно это не рекомендуется:

```r
import(data.table)
```

**Причины, по которым полный импорт лучше не делать:**
1. **Документация**: Файл NAMESPACE может служить хорошей документацией о том, как Ваших зависимостей от других пакетов.
2. **Избежание конфликтов**: Полный импорт может привести к ошибкам, которые сложно диагностировать и исправлять. Например, если вы используете `import(pkgA)` и `import(pkgB)`, но позже `pkgB` экспортирует функцию, которую уже экспортирует `pkgA`, это приведет к поломке вашего пакета из-за конфликтов в вашем пространстве имен, что запрещено `R CMD check` и CRAN.

### Шаг 3: Обновите файлы кода R вне директории R/ Вашего пакета

Когда вы перемещаете пакет из `Depends` в `Imports`, он больше не будет
автоматически подключаться при подключении вашего пакета. Это может быть
важно для примеров, тестов, руководств и демо-файлов, где пакеты, указанные
в `Imports`, должны быть подключены явным образом.

**До (с `Depends`):**
```r
# функции data.table доступны напрямую
library(MyPkgDependsDataTable)
dt <- data.table(x = 1:10, y = letters[1:10])
setDT(dt)
result <- merge(dt, other_dt, by = "x")
```

**После (с использованием `Imports`):**
```r
# Явным образом загрузить data.table в пользовательских скриптах или руководствах пакетов
library(data.table)
library(MyPkgDependsDataTable)
dt <- data.table(x = 1:10, y = letters[1:10])
setDT(dt)
result <- merge(dt, other_dt, by = "x")
```

### Преимущества использования `Imports`
- **Удобство для пользователей**: `Depends` изменяет путь `search()` ваших
  пользователей, возможно, без их желания.
- **Управление пространством имен**: Доступны только те функции, которые ваш
  пакет явно импортирует, что снижает риск пересечения имен функций.
- **Более чистая загрузка пакетов**: Зависимости вашего пакета не
  оказываются внутри `search()`, что делает процесс загрузки чище и
  потенциально быстрее.
- **Упрощение обслуживания**: Это упрощает задачи обслуживания по мере
  развития API Ваших зависимостей. Слишком сильная зависимость от `Depends`
  может со временем привести к конфликтам и проблемам совместимости.
