【R】スコープを限定してパッケージをインポートする

概要

R でパッケージを使うとき、library()require() でロードする代わりに import::here()box::use() を使うとスコープを限定してパッケージの関数をインポートすることができます。これは関数の副作用を避けるのに役立ちます。

課題

例えば、次のような関数 process_data() を考えます。この関数は dplyr パッケージの関数を呼び出します

process_data <- function() {
  # dplyr パッケージを使うため library() でロードする
  library(dplyr)

  # dplyr の関数を使う
  airquality %>% 
    filter(!is.na(Ozone)) %>% 
    select(Month, Day, Ozone, Temp)
}

この関数を実行すると、加工されたデータが戻り値として得られます:

data <- process_data()
head(data)
##   Month Day Ozone Temp
## 1     5   1    41   67
## 2     5   2    36   72
## 3     5   3    12   74
## 4     5   4    18   62
## 5     5   6    28   66
## 6     5   7    23   65

このとき、サーチパスを調べてみると

search()
##  [1] ".GlobalEnv"        "package:dplyr"     "package:stats"    
##  [4] "package:graphics"  "package:grDevices" "package:utils"    
##  [7] "package:datasets"  "package:methods"   "Autoloads"        
## [10] "package:base"

のように "package:dplyr" が含まれていることがわかります。

"package:dplyr" %in% search()
## [1] TRUE

関数 process_data() を実行する前はオブジェクト filterstats パッケージの関数を意味していました。しかし、関数 process_data() を実行したことによってサーチパスが変わったため dplyr パッケージの filter 関数という意味に変わってしまいました:

environment(filter)
## <environment: namespace:dplyr>

このように、関数の中で library()require() を実行すると、その関数の外でもサーチパスに影響してしまいます。 関数を実行する前後でサーチパスが変化してしまうと他の処理に予期しない影響が出る可能性があるので、このような副作用は好ましくありません。

常に ダブルコロン演算子 :: を使って dplyr::filterdplyr::select のように パッケージ名::関数名 の形で呼び出せばサーチパスへの副作用は避けられます。 実際、パッケージ開発プロジェクトでは一般的にこのようなスタイルが推奨されています。

しかし、関数の呼び出しが多くなるとこのスタイルではコード量が増え煩雑になってきます。できれば繰り返しパッケージ名を書くのは避けてコードをシンプルに保つ方法が欲しいところです。

import::here()

import は Python の import 文のような文法でパッケージから関数をインポートできるようになるパッケージです。 このパッケージに含まれる import::here() を使うと、そのスコープに限って関数がインポートされます。

例えば

# 関数を定義する
process_data <- function() {
  # パッケージから関数をインポートする
  import::here(dplyr, "%>%", filter, select)
  
  # インポートした関数を使う
  airquality %>% 
    filter(!is.na(Ozone)) %>% 
    select(Month, Day, Ozone, Temp)
}

のように関数を定義し、この関数を実行してみます:

# 定義した関数を使う
data <- process_data()

このとき、サーチパスに "package:dplyr" は含まれていません:

"package:dplyr" %in% search()
## [1] FALSE

よって、関数 process_data() の外ではオブジェクト filter はパッケージ stats の関数を意味したままです:

environment(filter)
## <environment: namespace:stats>

このように、import::here() を使うとサーチパスへの副作用を抑えることができます。

box::use()

box パッケージの box::use() でも import::here() と同じようにインポートの影響をスコープ内に限定することができます。

例えばこのように関数を定義して使用します:

# 関数を定義する
process_data <- function() {
  # パッケージから関数をインポートする
  box::use(dplyr[`%>%`, filter, select])
  
  # インポートした関数を使う
  airquality %>% 
    filter(!is.na(Ozone)) %>% 
    select(Ozone, Temp, Month, Day)
}

# 定義した関数を使用する
data <- process_data()

この関数もサーチパスに影響を与えません:

"package:dplyr" %in% search()
## [1] FALSE
environment(filter)
## <environment: namespace:stats>

(補足) withr::with_package()

実は withr::with_package() でも同様のことができます:

process_data <- function() {
  withr::with_package("dplyr", {
    airquality %>% 
      filter(!is.na(Ozone)) %>% 
      select(Ozone, Temp, Month, Day)
  })
}

ただし withr::with_package() で複数のパッケージをロードするにはコードを入れ子にしなければならないのと、どの関数をインポートするかを指定できないので、import::here()box::use() のほうが使いやすいように思われます。

まとめ

関数の内部で library()require() を使うと、関数の外部のサーチパスにも影響が出てしまいます。常に パッケージ名::関数名() の形で関数を呼び出せばサーチパスの問題は避けられますが、コード量が増え煩雑になってしまう懸念があります。import::here()box::use() を使えばサーチパスへの副作用を避けながらコードをシンプルに保つ事ができます。

副作用のない関数を定義し、それを部品として組み合わせてシステム全体を構築するのが関数型プログラミングのベストプラクティスとされています。そうすることでコードが理解しやすく、また再利用しやすいものになります。本記事ではサーチパスに注目しましたが、importbox には他にもスクリプトをモジュール化するための機能が用意されています。これも規模の大きなコードを分割して整理するのに役立つでしょう。

参考資料