関数の lifecycle の変化を伝える

lifecycle は、関数や引数の lifecycle ステージを文書化する標準的な方法と、非推奨の関数からユーザーを誘導するためのツールを提供します。 詳細な説明に入る前に、 vignette("stages") で説明されている lifecycle ステージに精通していることを確認してください。

基本

lifecycle バッジは、ユーザーがドキュメントを読むときに、lifecycle ステージを簡単に確認できるようにします。 バッジを使うには、まず usethis::use_lifecycle() を呼んでバッジの画像をパッケージに埋め込みます (これは一度だけ行う必要があります)。次に lifecycle::badge() を使ってバッジを挿入します。

#' `r lifecycle::badge("experimental")`
#' `r lifecycle::badge("deprecated")`
#' `r lifecycle::badge("superseded")`

また、非推奨の関数は、実行時にその状態を告知する必要があります。 lifecycle は、3つの主要な引数を取る deprecate_warn() を提供します。

詳細は後ほど説明しますが、ここではいくつかの使用例を紹介します。

lifecycle::deprecate_warn("1.0.0", "old_fun()", "new_fun()")
#> Warning: `old_fun()` was deprecated in lifecycle 1.0.0.
#> Please use `new_fun()` instead.
lifecycle::deprecate_warn("1.0.0", "fun()", "testthat::fun()")
#> Warning: `fun()` was deprecated in lifecycle 1.0.0.
#> Please use `testthat::fun()` instead.
lifecycle::deprecate_warn("1.0.0", "fun(old_arg)", "fun(new_arg)")
#> Warning: The `old_arg` argument of `fun()` is deprecated as of lifecycle 1.0.0.
#> Please use the `new_arg` argument instead.

(メッセージにパッケージ名が含まれていることに注意してください。これは、関数を呼び出した環境から自動的に検出されるため、パッケージの名前空間から関数が呼び出されない限り機能しません)。

次のセクションでは、lifecycle バッジと関数を一緒に使用して、さまざまな一般的な開発タスクを処理する方法について説明します。

関数

関数を非推奨

まず、@descriptionブロックにバッジを追加します1。 なぜ非推奨になったのか、そして代わりに何を使えばいいのかを簡単に説明します。

#' Add two numbers
#' 
#' @description
#' `r lifecycle::badge("deprecated")`
#' 
#' This function was deprecated because we realised that it's
#' a special case of the [sum()] function.

次に、古い使い方から新しい使い方への変換方法を示す例を更新します。

#' @examples 
#' add_two(1, 2)
#' # ->
#' sum(1, 2)

そして、@keywords internalを追加して、ドキュメントのインデックスからその関数を削除してください。 pkgdown を使用している場合は、 _pkgdown.yml に記載されなくなったことも確認してください。 これらの変更により、新しいユーザが非推奨の関数に出会う可能性は低くなりますが、すでにその関数を知っているユーザがドキュメントを参照することは防げません。

#' @keywords internal

これで docsの作成は終わりです。次は、ユーザーが関数を呼び出したときに警告を表示するようにしましょう。 関数の最初の行に deprecate_warn() の呼び出しを追加して、これを行います。

add_two <- function(x, y) {
  lifecycle::deprecate_warn("1.0.0", "add_two()", "base::sum()")
  x + y
}

add_two(1, 2)
#> Warning: `add_two()` was deprecated in lifecycle 1.0.0.
#> Please use `base::sum()` instead.
#> [1] 3

deprecate_warn() は、2つの一般的な非推奨の選択肢について、ユーザーフレンドリーなメッセージを生成します。

その他のケースでは、 details 引数を使用して、ユーザーへの独自のメッセージを提供します。

add_two <- function(x, y) {
  lifecycle::deprecate_warn(
    "1.0.0", 
    "add_two()", 
    details = "This function is a special case of sum(); use it instead."
  )
  x + y
}

add_two(1, 2)
#> Warning: `add_two()` was deprecated in lifecycle 1.0.0.
#> This function is a special case of sum(); use it instead.
#> [1] 3

非推奨の関数がまだ動作し、有用な警告を生成することをテストし、非推奨を正しく実装したことをテストすることは良い習慣です。 testthat::expect_snapshot()2 の中で期待値を使うのは、これを行うための便利な方法です。

test_that("add_two is deprecated", {
  expect_snapshot({
    x <- add_two(1, 1)
    expect_equal(x, 2)
  })
})

If you have existing tests for the deprecated function you can suppress the warning in those tests with the lifecycle_verbosity option:

非推奨の関数に対するテストが既にある場合は、 lifecycle_verbosity オプションでテストでの警告を抑制することができます。

test_that("add_two returns the sum of its inputs", {
  withr::local_options(lifecycle_verbosity = "quiet")
  expect_equal(add_two(1, 1), 2)
})

そして、非推奨に特化した別のテストを追加してください。

test_that("add_two is deprecated", {
  expect_snapshot(add_two(1, 1))
})

段階を踏んだ非推奨化

特に重要な関数については、非推奨化処理にもう2つの段階を追加することができます。

これらのステージを使用する場合、メジャーリリースとマイナーリリースの非推奨ステージをバンプするためのプロセスも必要になるでしょう。 私たちは次のようなものを推奨します。

  1. deprecate_stop() を検索し、その関数を完全に削除する準備ができたかどうか検討します。

  2. deprecate_warn() を検索して、deprecate_stop() に置き換えます。 残りの関数本体とテストを削除します。

  3. deprecate_soft() を検索して、 deprecate_warn() に置き換えます。

関数の名前を変更

既存のコードを壊さずに関数名を変更するには、実装を新しい関数に移動し、非推奨のメッセージを添えて古い関数から新しい関数を呼び出します。

#' Add two numbers
#' 
#' @description 
#' `r lifecycle::badge("deprecated")`
#' 
#' `add_two()` was renamed to `number_add()` to create a more
#' consistent API.
#' @keywords internal
#' @export
add_two <- function(foo, bar) {
  lifecycle::deprecate_warn("1.0.0", "add_two()", "number_add()")
  number_add(foo, bar)
}

# documentation goes here...
#' @export
number_add <- function(x, y) {
  x + y
}

もし、APIのオーバーホールの一環として多くの関数の名前を変更するのであれば、このように1つのファイルにすべての変更を記録するのも良いでしょう。 https://rvest.tidyverse.org/reference/rename.html

旧式の関数

警告を発してユーザーを誘導する必要がないため、関数を廃止するよりも簡単です。 そのため、必要なことは、旧式 (supersed) バッジを追加することだけです。

#' Gather columns into key-value pairs
#'
#' @description
#' `r lifecycle::badge("superseded")`

そして、なぜその機能が廃止されたのか、推奨される代替案は何なのかを説明します。

#'
#' Development on `gather()` is complete, and for new code we recommend
#' switching to `pivot_longer()`, which is easier to use, more featureful,
#' and still under active development.
#' 
#' In brief,
#' `df %>% gather("key", "value", x, y, z)` is equivalent to
#' `df %>% pivot_longer(c(x, y, z), names_to = "key", values_to = "value")`.
#' See more details in `vignette("pivot")`.

残りのドキュメントはそのままで大丈夫です。

もし、lifecycle の最先端を生きたいのであれば、実験的な signal_stage() の呼び出しを追加してください。

gather <- function(data, key = "key", value = "value", ...) {
  lifecycle::signal_stage("superseded", "gather()")
}

このシグナルは現在どのような挙動にもフックされていませんが、将来のリリースではロギングと解析のツールを提供する予定です。

機能を実験的にする

ある関数が実験的であり、将来的にインターフェースが変更される可能性があることを宣伝するために、まず説明に実験的 (experimental) バッジを追加してください。

#' @description
#' `r lifecycle::badge("experimental")`

もしその関数が非常に実験的なものであれば、@keywords internal も追加した方がよいかもしれません。

もし、実験的なlifecycle 機能を試したいのであれば、 signal_stage() の呼び出しを本文に追加してください。

cool_function <- function() {
  lifecycle::signal_stage("experimental", "cool_function()")
}

このシグナルは現在、どのような挙動にもフックされていませんが、将来のリリースではロギングや分析ツールを提供する予定です。

引数

引数を非推奨とし、既存のデフォルトを維持する。

この例では、na.rm を非推奨とし、常に TRUE とすることにします。

add_two <- function(x, y, na.rm = TRUE) {
  sum(x, y, na.rm = na.rm)
}

まず、引数の説明文にバッジを追加します。

#' @param na.rm `r lifecycle::badge("deprecated")` `na.rm = FALSE` is no
#'   longer supported; this function will always remove missing values

また、na.rmがFALSEの場合、非推奨の警告を追加します。 この場合、動作に代わるものはないので、代わりに details を使ってカスタムメッセージを提供します。

add_two <- function(x, y, na.rm = TRUE) {
  if (!isTRUE(na.rm)) {
    lifecycle::deprecate_warn(
      when = "1.0.0", 
      what = "add_two(na.rm)",
      details = "Ability to retain missing values will be dropped in next release."
    )
  }
  
  sum(x, y, na.rm = na.rm)
}

add_two(1, NA, na.rm = TRUE)
#> [1] 1
add_two(1, NA, na.rm = FALSE)
#> Warning: The `na.rm` argument of `add_two()` is deprecated as of lifecycle 1.0.0.
#> Ability to retain missing values will be dropped in next release.
#> [1] NA

引数を非推奨とし、新しいデフォルトを提供

また、デフォルト値を lifecycle::deprecated() に変更して、外から見ても非推奨であることがわかるようにし、 lifecycle::is_present() を使って引数が提供されたかどうかをテストすることもできます。 missing()` とは異なり、これは直接的な呼び出しと間接的な呼び出しの両方で機能します。

#' @importFrom lifecycle deprecated
add_two <- function(x, y, na.rm = deprecated()) {
  if (lifecycle::is_present(na.rm)) {
    lifecycle::deprecate_warn(
      when = "1.0.0", 
      what = "add_two(na.rm)",
      details = "Ability to retain missing values will be dropped in next release."
    )
  }
  
  sum(x, y, na.rm = na.rm)
}

この手法の最大の利点は、ユーザが na.rm のどの値を使っても警告が表示されることです。

add_two(1, NA, na.rm = TRUE)
#> Warning: The `na.rm` argument of `add_two()` is deprecated as of lifecycle 1.0.0.
#> Ability to retain missing values will be dropped in next release.
#> [1] 1
add_two(1, NA, na.rm = FALSE)
#> Warning: The `na.rm` argument of `add_two()` is deprecated as of lifecycle 1.0.0.
#> Ability to retain missing values will be dropped in next release.
#> [1] NA

引数の名前を変更

引数の名前を間違えたことに気づいたとき、引数の名前を変更することがあります。 例えば、引数の複合名を区切るのに、誤って _ ではなく . を使っていることに気づいたとします。 一時的に両方の引数を許可して、ユーザーが古い引数を入力したときに非推奨の警告を表示する必要があります。

add_two <- function(x, y, na_rm = TRUE, na.rm = deprecated()) {
  if (lifecycle::is_present(na.rm)) {
    lifecycle::deprecate_warn("1.0.0", "add_two(na.rm)", "add_two(na_rm)")
    na_rm <- na.rm
  }
  
  add_two(x, y, na.rm = na_rm)
}

引数に許容される入力を減らす

許可される入力のセットを狭めるには、ユーザーが以前にサポートされていた入力を供給するときだけ deprecate_warn() を呼び出します。 以前の挙動を保持することを確認してください。

add_two <- function(x, y) {
  if (length(y) != 1) {
    lifecycle::deprecate_warn("1.0.0", "foo(y = 'must be a scalar')")
    y <- sum(y)
  }
  x + y
}

add_two(1, 2)
#> [1] 3
add_two(1, 1:5)
#> Warning: The `y` argument of `foo()` must be a scalar as of lifecycle 1.0.0.
#> [1] 16

  1. これらの例のように、説明が複数の段落になる場合のみ、明示的な @description を使用します。↩︎

  2. スナップショットテストについては vignette("snapshotting", package = "testthat") で学ぶことができます。↩︎