dplyr 互換性

この vignette は、dplyr の後方互換性のない変更のためにコードを更新する必要のあるパッケージ作者を対象としています。私たちは後方互換性のない変更をできるだけ少なくしようとしていますが、既存のコードを根本的に単純化したり、将来的に多くの潜在的な価値を引き出すために、後方互換性のない変更が必要になることもあります。

この vignette は、複数のバージョンの dplyr で動作するパッケージコードを書くための一般的なアドバイスから始まり、dplyr のバージョンにおける具体的な変更点について説明していきます。

複数のdplyrバージョンでの作業

理想的には、あなたのパッケージが dplyr のリリース版と開発版の両方で動作することを確認したいものです。これは一般的に少し手間がかかりますが、2つの大きな利点があります。

  1. ユーザーにとっては便利です。 dplyr をアップデートしたくなければ、強制的にアップデートさせられることがないので、ユーザーにとって便利です。

  2. 複数のパッケージをまとめてリリースする必要がないので、CRANでの作業が楽になります。

あるパッケージの複数のバージョンでコードを動作させるための最初のツールは、シンプルな if 文です。:

if (utils::packageVersion("dplyr") > "0.5.0") {
  # code for new version
} else {
  # code for old version
}

常に、> current-version を使用してください。 >= next-version は使用しないでください。 これは、このブランチがパッケージの開発バージョンにも使用されることを保証するためです。例えば、現在のリリースがバージョン「0.5.0」であれば、開発バージョンは「0.5.0.9000」となります。

時には、NAMESPACE が変更され、条件付きで異なる関数をインポートする必要がある状況に遭遇することがあります。 これは、関数があるパッケージから別のパッケージに移されたときによく起こります。 私たちは自動的なフォールバックを提供するように最善を尽くしていますが、常に可能というわけではありません。 多くの場合、importFrom を避け、代わりに :: を使用することで問題を回避することができます。 可能な限りこのようにしてください。:

if (utils::packageVersion("dplyr") > "0.5.0") {
  dbplyr::build_sql(...)
} else {
  dplyr::build_sql(...)
}

これにより、R CMD checkというノートが生成されますが、これは問題ありません。 単純に、コードが後方互換であることを確認するためにラッパーを書いたためにノートが発生したと説明してください。

時には、importFrom() を避けることができないこともあります。 例えば、generic のメソッドを定義するために generic をインポートする場合です。 この場合、NAMESPACE ファイルのあまり知られていない機能を利用することができます: if 文を含めることができます。

#' @rawNamespace
#' if (utils::packageVersion("dplyr") > "0.5.0") {
#'   importFrom("dbplyr", "build_sql")
#' } else {
#'   importFrom("dplyr", "build_sql")
#' }

dplyr 0.6.0

データベースのコードが dbplyr に移動

ほとんどのデータベース関連のコードが dplyr から移動し、新しいパッケージ dbplyr になりました。 これにより、dplyr がよりシンプルになり、データベースにのみ影響するバグの修正プログラムのリリースが容易になります。 もし、dplyr にデータベースバックエンドを実装しているのであれば、バックエンドに関するbackend newsをお読みください。

どのような generic を使用するか、またどのような generic がメソッドを提供するかによって、いくつかの条件付きコードを書く必要があるかもしれません。 これを簡単にするために、ヘルパーコードを書いてくれる wrap_dbplyr_obj() を書きました。

wrap_dbplyr_obj("build_sql")

wrap_dbplyr_obj("base_agg")

この関数の結果をあなたのパッケージにコピーするだけです。

これらは R CMD check NOTES を生成するので、後方互換性を確保するためであることを CRAN に伝えるようにしてください。

アンダースコア付き verbs_() の非推奨化

tidyeval フレームワークでは、SE と NSE のセマンティクスを同じ関数内で組み合わせることができるようになったため、アンダースコア付きの動詞は 静かに廃止されました。

SE_動詞をお使いの方へ

従来のアンダースコア付きバージョンでは、オブジェクトを取得する際に lazyeval::as.lazy() メソッドが定義されているオブジェクトを取ります。 これは、シンボルや呼び出し、文字列、数式などが含まれます。 これらのオブジェクトはすべて quosure に置き換えられ、引用符のない quosure で tidyeval の動詞を呼び出せるようになりました。:

quo <- quo(cyl)
select(mtcars, !! quo)

シンボル式もサポートされていますが、base のシンボルやコールはスコープ情報を持たないことに注意してください。 データフレーム内のオブジェクトを参照している場合は、エンクロージャの指定を省略しても問題ありません。:

sym <- quote(cyl)
select(mtcars, !! sym)

call <- quote(mean(cyl))
summarise(mtcars, cyl = !! call)

物体を quosure に変換することは、一般的に簡単です。現在の環境で囲うには、quo() で直接アンクオートするか、as_quosure()を使います。:

quo(!! sym)
#> <quosure>
#> expr: ^cyl
#> env:  global
quo(!! call)
#> <quosure>
#> expr: ^mean(cyl)
#> env:  global

rlang::as_quosure(sym)
#> <quosure>
#> expr: ^cyl
#> env:  global
rlang::as_quosure(call)
#> <quosure>
#> expr: ^mean(cyl)
#> env:  global

数式と quosure は非常によく似たオブジェクトですが(そして最も一般的な意味では、数式は quosure です)、tidyeval 関数の中で互換的に使用することはできないことに注意してください。初期の実装では裸の数式を quosure として扱っていましたが、これは stats パッケージのモデリング関数との互換性の問題を引き起こしました。幸いなことに、数式を tidyeval 関数で自己評価する quosure に変換するのは簡単です。:

f <- ~cyl
f
#> ~cyl
rlang::as_quosure(f)
#> <quosure>
#> expr: ^cyl
#> env:  global

最後に、おそらく最も重要なことですが、文字列は解析できませんし、すべきではありません。 を解析することはできません。開発者としては、文字列を使って問題を解決しようとしたくなります。 文字列を使って問題を解決しようとしたくなります。 文字列を使って問題を解決しようとしたくなります。しかし、それはほとんどの場合、間違った方法で 問題に取り組む方法としては間違っています。ただし、シンボルを作成する場合は例外です。 シンボルを作成する場合は例外です。この場合、文字列を使用することは完全に正当な方法です。:

rlang::sym("cyl")
#> cyl
rlang::syms(letters[1:3])
#> [[1]]
#> a
#> 
#> [[2]]
#> b
#> 
#> [[3]]
#> c

しかし、コールの作成に文字列を使ってはいけません。その代わりに準引用符を使うことができます。:

syms <- rlang::syms(c("foo", "bar", "baz"))
quo(my_call(!!! syms))
#> <quosure>
#> expr: ^my_call(foo, bar, baz)
#> env:  global

fun <- rlang::sym("my_call")
quo((!!fun)(!!! syms))
#> <quosure>
#> expr: ^my_call(foo, bar, baz)
#> env:  global

または、call2()で呼び出しを作成します。:

call <- rlang::call2("my_call", !!! syms)
call
#> my_call(foo, bar, baz)

rlang::as_quosure(call)
#> <quosure>
#> expr: ^my_call(foo, bar, baz)
#> env:  global

# Or equivalently:
quo(!! rlang::call2("my_call", !!! syms))
#> <quosure>
#> expr: ^my_call(foo, bar, baz)
#> env:  global

なお、interp() に基づくイディオムは一般的に避け、準引用符で置き換えるべきです。 今まで補間していたところを:

lazyeval::interp(~ mean(var), var = rlang::sym("mpg"))

クオーテーションは外します:

var <- "mpg"
quo(mean(!! rlang::sym(var)))

quasiquotation と quosure 詳細については、vignette("programming") を参照してください。

パッケージ作成者向け

パッケージ作成者のために、rlang は互換性ファイル](https://github.com/r-lib/rlang/blob/master/R/compat-lazyeval.R)を提供していますcompat_lazy()compat_lazy_dots() は遅延可能なオブジェクトを適切な quosure に変えます。 例えば、dplyr 0.6でアンダースコア版の filter() を定義した方法は以下の通りです。:

filter_.tbl_df <- function(.data, ..., .dots = list()) {
  dots <- compat_lazy_dots(.dots, caller_env(), ...)
  filter(.data, !!! dots)
}

tidyeval では、正しいメソッドへの S3 のディスパッチが問題となる場合があります。 過去には、dplyr の動詞のジェネリシティはアンダースコアのバージョンでディスパッチすることで実現されていました。 アンダースコアが廃止された今、アンダースコアのない動詞を S3 generic にしました。

新しい S3 generic のデフォルトメソッドで古いアンダースコア付きの動詞を再ディスパッチすることで、後方互換性を維持しています。 例えば、以下は filter() を再ディスパッチする方法です。:

filter.default <- function(.data, ...) {
  filter_(.data, .dots = compat_as_lazy_dots(...))
}

これでほとんどの場合、仕事は完了します。 しかし、アンダースコアのないメソッドを提供しているクラス(data.frame, tbl_df, tbl_cube, grouped_df)を継承しているオブジェクトでは、デフォルトのメソッドは呼び出されません。 この例として、オブジェクトがクラス c("sf", "data.frame") を持つ sf パッケージがあります。 このようなパッケージの作者は、dplyr との互換性を保つために、アンダースコアではない generic のためのメソッドを提供する必要があります。:

filter.sf <- function(.data, ...) {
  st_as_sf(NextMethod())
}

この件で助けが必要な場合は、ぜひご連絡ください。

mutate_each() および summarise_each() の非推奨。

これらの関数は、より完全な関数群に置き換えられました。 この関数群は、接尾辞に _if, _at, _all を持ち、mutate, summarise 以外の動詞も含まれています。

コードを新しいファミリーにアップデートする必要がある場合、どの変数に funs() を適用するかによって、2つの関連する関数があります。 変数の選択をせずに mutate_each() を呼び出した場合、funs はすべての変数に適用されます。 この場合、代わりに mutate_all() を使うようにコードを更新する必要があります。:

mutate_each(starwars, funs(as.character))
mutate_all(starwars, funs(as.character))

なお、新しい動詞は base 関数もサポートしていますので、必ずしも funs() でラップする必要はありません。:

mutate_all(starwars, as.character)

一方,変数の選択を行った場合には,mutate_at() を使うべきでしょう。 変数選択は vars() でラップしなければなりません。:

mutate_each(starwars, funs(as.character), height, mass)
mutate_at(starwars, vars(height, mass), as.character)

vars() は,通常の select() で使用されるすべての選択ヘルパーをサポートしています。:

summarise_at(mtcars, vars(starts_with("d")), mean)

なお、vars() の選択の代わりに、列名の文字ベクトルを指定することもできます。:

mutate_at(starwars, c("height", "mass"), as.character)