Rのパッケージをつくるとき、NAMESPACEにimportFromを書くか::を使うのかどっちがいいかという回りくどい話

個人的な見解を書いておきます。

まず、パッケージの中で別パッケージの関数を使うにはどうすればいいか、おさらいしてみましょう。

DESCRIPTIONファイル

まずは、install.packages()によって依存するパッケージが自動でインストールされるようにするため、DESCRIPTIONファイルに必要なパッケージは列挙する必要があります。

オフィシャルな説明はWriting R Extensions - 1.1.3 Package Dependenciesあたりに書かれています。パッケージの依存関係を記述するフィールド6つ定義されていますが、主に使うのは以下の3つです。

  • Depends: パッケージ全体をアタッチする必要がある*1、もしくはアタッチした方がいい(後述)パッケージはここに書きます。自動でインストールされます。
  • Imports: そのパッケージの関数を使うだけでよければここに書きます。自動でインストールされます。
  • Suggests: そのパッケージの関数を使うだけでよく、なおかつパッケージがインストールされていなくても問題ない(例やテストでしか使わない、インストールされていなければ別の処理を行う、など)ならここに書きます。dependencies = TRUEを指定しないとインストールされません。

大概の場合、欲しいのはパッケージ読み込み時の効果ではなくその中に定義されている関数なので、Importsが適切でしょう。

また、Dependsは自動でパッケージを読み込んでくれるので便利なように思えますが、結局、安全に関数をインポートするにはNAMESPACEファイルにimportFrom(もしくはimport、もしくは::を使う)しないといけません。ちょっと回り道してそのあたりを見てみましょう。

Dependsの自動読み込みでは安心できない

この記事を書くために調べていて以下の質問を見つけました。

曰く、Dependsはパッケージを読み込んでくれるので、必要な関数がサーチパス上にあることは保証してくれますが、必ずそれが選ばれることは保証してくれません。

具体例として、magrittrパッケージの関数extract()を内部で使う関数をもつパッケージをつくってみました。

具体的にはこういう関数です。xから要素の1つ目を取り出します。

#' @export
take1 <- function(x) {
  extract(x, 1)
}

DESCRIPTIONファイルにはこう書いておきます。

Depends:
  magrittr

すると、以下は意図通り動きます。

# インストール
devtools::install_github("yutannihilation/dependencyA")
library("dependencyA")

take1(1:10)
#> [1] 1

しかし、ご存知の通りRでは後から読み込まれたパッケージの方が優先されます。ここで同じextract()という名前の関数を持つtidyrパッケージを読み込んでもう一度やってみましょう。

library("tidyr")

take1(1:10)
#> Error: Invalid column specification

エラーになってしまいました。これはmagrittrのextract()ではなくてtidyrのextract()が呼ばれてしまうためです。

::を使う

解決策としては、::を使ってパッケージを明示するというのがひとつの手です。先ほどの関数をこう書き換えれば、tidyrパッケージが読み込まれた後も正しくmagrittrパッケージのextract()の方が使われます。

#' @export
take1 <- function(x) {
  magrittr::extract(x, 1)
}

ただし、これは関数ひとつなのでいいですが、毎回::を書くのはだるい、という問題があります。

あと、些細な問題ではありますが、::を使うと5ナノマイクロ秒(追記:コメント欄で指摘いただきました。すみません…)程度のオーバーヘッドがあるためです。これはR Packagesでも言及されています。

There’s also a minor performance penalty associated with :: (on the order of 5µs, so it will only matter if you call the function millions of times). (http://r-pkgs.had.co.nz/description.html)

なぜこのオーバーヘッドがあるかというと、::も関数だからです。この関数呼び出しのオーバーヘッドがあります(ということだと思ってるんですけど違ったら誰かツッコミを…)

`::`
#> function (pkg, name) 
#> {
#>     pkg <- as.character(substitute(pkg))
#>     name <- as.character(substitute(name))
#>     getExportedValue(pkg, name)
#> }
#> <bytecode: 0x000000000be7f658>
#> <environment: namespace:base>

完全に余談ですが、::が関数だというのは%>%を使うときにたまにひっかります。こういうエラーに遭遇したことはないでしょうか?

iris %>% utils::head
#> Error in .::utils : unused argument (head)

これは、%>%によってirisが引数として挿入される先がhead()ではなく::になっているからです。つまり上では、

`::`(iris, utils, head)

という関数呼び出しが実行されようとして、引数がひとつ多いのでエラーになっています。エラーにならないようにするにはutils::headではなくutils::head()とする必要があります。

NAMESPACEファイルにimportFromを書く

話がそれました。::と毎回書くのがだるい、という話でした。

他の手として、NAMESPACEファイルにimportFromを書くという手があります。こうです。

importFrom(magrittr,extract)

(https://github.com/yutannihilation/dependencyA/blob/use-imports/NAMESPACE#L4)

NAMESPACEファイルをroxygen2を任せているなら、Rファイルのどこかに@importFromディレクティブを書いておけばインポートできます。

#' @importFrom magrittr extract

(https://github.com/yutannihilation/dependencyA/blob/use-imports/R/take1.R#L1)

これを書いておくと、ひとつ上の親環境にextractが見つかるようになります。ユーザがlibrary()でパッケージを読み込んでもこっちの方が優先順位が上なので安心です。

library("dependencyA")

e <- environment(take1)
e
#> <environment: namespace:dependencyA>

pe <- parent.env(e)
pe
#> <environment: 0x0000000014e558b0>
#> attr(,"name")
#> [1] "imports:dependencyA"

ls(pe)
#> [1] "extract"

ということでDependsじゃなくてImportsの方がいい

ということで、::importFromを書く必要はあるので、Dependsを使うメリットはあまりありません。変にパッケージを読み込んでグローバル環境を汚染するより、Importsにしておく方が無難でしょう。

ちなみに、Dependsについては最近はあまり使われてない、と断りつつ、数少ないメリットについて言及しています。

Field ‘Depends’ should nowadays be used rarely, only for packages which are intended to be put on the search path to make their facilities available to the end user (and not to the package itself): for example it makes sense that a user of package latticeExtra would want the functions of package lattice made available.
(https://cran.r-project.org/doc/manuals/r-release/R-exts.html#Package-Dependencies)

あるパッケージを拡張するようなパッケージの場合、拡張される側の方のパッケージを読み込んどいた方が便利だよね、という話のようです。

しかし、これも最近の流行(というかHadley流?)のやり方としては、そういう感じでユーザが直接使えた方がいい別パッケージの関数はimportFromしたやつをそのままexportするという手が取られています。例えば、dplyrパッケージを読み込むだけでmagrittrパッケージの%>%演算子が使えますが、これは以下のように指定されているからです。

#' @importFrom magrittr %>%
#' @export
magrittr::`%>%`

(https://github.com/tidyverse/dplyr/blob/e5b7d00555148c8d46f61e3cd2e46dd52a0f4b28/R/utils.r#L1-L3)

ここまでのまとめ

DESCRIPTIONファイルのImportsNAMESPACEファイルのimportFrom/importを混同しがちですが、イメージ的には、

  • DESCRIPTIONファイルのImports: そのパッケージをインストールするかどうかを決める
  • NAMESPACEファイルのimportFrom/import: 読み込み方をどうするかを決める

という感じです。これは、install.packages()でパッケージをインストールしてもlibrary()で読み込まなければ使えないし、逆にlibrary()してもそのパッケージがインストールされていなければエラーになる、というのと同じ関係です。どちらも必要なのです。

importFrom::かどっちがいいのか

ここで、R Packagesを見てみましょう。

If you are using just a few functions from another package, my recommendation is to note the package name in the Imports: field of the DESCRIPTION file and call the function(s) explicitly using ::, e.g., pkg::fun().

If you are using functions repeatedly, you can avoid :: by importing the function with @importFrom pgk fun. This also has a small performance benefit, because :: adds approximately 5 µs to function evaluation time. (http://r-pkgs.had.co.nz/namespace.html#imports)

ちょっとだけなら::で、いっぱいあるときはimportFrom、という基準のようです。

importFromを使うのがちゃんとしているとは思います。明示的に「この関数を使います」と宣言してからその関数を使っていくのが正しい作法で、::アドホックにいろいろ関数を継ぎ足していくのはなんかルーズに感じます。あと、::がいっぱいあるとなんか読みづらいというのはあるでしょう。

それでも、個人的なオススメは::です。その理由は、

  • importFromをちゃんと書くという習慣を身につけるのはけっこう大変です。手元では動いてるからとGitHubにプッシュしたあとCIのエラーで気づく、みたいな感じになりがちです。それよりは、「別のパッケージの関数を使うときは::」という方が方針として分かりやすいんじゃないかな、と思っています。
  • これはIDE的な都合ですが、補完したりヘルプを見ながらやるときに::だとパッケージを読み込んでいなくても候補が出てくるので便利です。

という感じです(考えたけどあんまり思いつかなかった…)。前者に関しては、手元でちゃんとR CMD checkしろという話だし、::にしたところで結局書き忘れはあるので大した差はないかな、という気はしています。

とはいえ%>%とか中置演算子importFromしないと使えないし、Hadleyのコードとか見ててもどっちも使ってるし、明確にどっちかにそろえるというのも難しいのかも。

どうなんでしょう。

最後に

importFrom派からのマサカリを待っています。(ここまで書いたものの正直どっちがいいのかわからないので教えてください…)

*1:.onLoad()による読み込みフックが必要とかそういうとき