👌

Rのコーディングスタイル

2021/01/29に公開

The tidyverse style guideを参考にtidyverseで使用されているコーディングスタイルについてまとめる。

ファイル

名前

ファイル名には意味を持たせ.Rで終わらせる。特殊文字は使用せず、数字、文字、-(ハイフン)、_(アンダーバー)を使用する。

# Good
fit_models.R
utility_functions.R

# Bad
fit models.R
foo.r
stuff.r

ファイルを特定の順序で実行する必要がある場合は、ファイルの前に番号を付ける。 10を超えるファイルがある可能性が高い場合は、左のパッドをゼロにする。

00_download.R
01_explore.R
...
09_model.R
10_visualize.R

後々いくつかの手順を見逃したことに気づいた場合、02a02bなどを使用したくなるだろうが、じっとこらえてすべてのファイル名を変更したほうが良い。

大文字の使用に注意する。なぜならあなたもしくはあなたの協力者が大文字と小文字を区別しないファイルシステム(MicrosoftWindowsやOSX等)を使用している可能性があり、大文字と小文字を区別するリビジョン制御システムで問題が発生する可能性があるため。できる限りすべて小文字を使用し、決して同じファイル名で小文字と大文字のみが異なる名前を付けてはいけない。

編成

複数のファイルにまたがってコードを整理する方法を正確に説明するのは困難である。経験則としては、ファイルにその内容を連想させる簡潔な名前を付けることができれば、優れたファイル構成になると思われる。しかし、それは困難を極める。

内部構造

コメント行に-=を使用して読みやすいチャンクにファイルを分割する。

# Load data ---------------------------

# Plot data ---------------------------

スクリプトでアドオンパッケージを使用している場合は、ファイルの最初にそれらをすべて一度にロードする。 これは、コード全体にlibrary()呼び出しをかけたり、Rprofileなどのスタートアップファイルに読み込まれる非表示の依存関係を持つよりも透明性がある。

構文

オブジェクト名

変数名と関数名には小文字、数字、\(アンダーバー)を使用すべきである。オブジェクト名内の単語を区切るには_(アンダーバー)を使用する。

# Good
day_one
day_1

# Bad
DayOne
dayone

ベースRは関数名(contrib.url())とクラス名(data.frame)にドットを使用するが、S3オブジェクトシステム専用にドットを使わずにおくことをお勧めする。S3では、メソッドにfunction.classという名前が付けられており、関数名とクラス名にも.(ドット)を使用すると、as.data.frame.data.frame()のような紛らわしいメソッドになってしまう。

データは(model_2018,model_2019,model_2020)のような変数名に格納せず、リストやデータフレームの使用を検討する。

一般に変数名は名詞で、関数名は動詞である必要がある。簡潔で意味のある名前を付ける。

# Good
day_one

# Bad
first_day_of_the_month
djm1

可能な限り、一般的な関数や変数の名前を利用しない。

# Bad
T <- FALSE
c <- 10
mean <- function(x) sum(x)

空白

コンマ

通常の英語のように、常にコンマの後にスペースを入れる。

# Good
x[, 1]

# Bad
x[,1]
x[ ,1]
x[ , 1]

括弧

通常の関数呼び出しでは、括弧の内側または外側にスペースを入れない。

# Good
mean(x, na.rm = TRUE)

# Bad
mean (x, na.rm = TRUE)
mean( x, na.rm = TRUE )

ifforwhileを使用する場合は、()の前後にスペースを入れる。

# Good
if (debug) {
  show(x)
}

# Bad
if(debug){
  show(x)
}

関数の引数に使用した()の後にスペースを配置する。

# Good
function(x) {}

# Bad
function (x) {}
function(x){}

包含

包含演算子{{}}は、その特別な動作を強調するために、常に内部にスペースを入れる必要がある。

# Good
max_by <- function(data, var, by) {
  data %>%
    group_by({{ by }}) %>%
    summarise(maximum = max({{ var }}, na.rm = TRUE))
}

# Bad
max_by <- function(data, var, by) {
  data %>%
    group_by({{by}}) %>%
    summarise(maximum = max({{var}}, na.rm = TRUE))
}

中置演算子

ほとんどの中置演算子(==+-<-など)は常にスペースで囲む必要がある。

# Good
height <- (feet * 12) + inches
mean(x, na.rm = TRUE)

# Bad
height<-feet*12+inches
mean(x, na.rm=TRUE)

いくつか例外はあるが以下の場合はスペースで囲まない。

  • 優先順位の高い演算子::::::$@[ [[^、単項-、単項+:
# Good
sqrt(x^2 + y^2)
df$z
x <- 1:10

# Bad
sqrt(x ^ 2 + y ^ 2)
df $ z
x <- 1 : 10
  • 右側が単一の識別子である場合の片側式
~foo
tribble(
  ~col1, ~col2,
  "a",   "b"
)

# Bad
~ foo
tribble(
  ~ col1, ~ col2,
  "a", "b"
)

複雑な右辺を持つ片側式にはスペースが必要

# Good
~ .x + .y

# Bad
~.x + .y
  • tidy evaluationで使用される!!!!!(単項-/+と同等の優先順位のため)
# Good
call(!!xyz)

# Bad
call(!! xyz)
call( !! xyz)
call(! !xyz)
  • ヘルプ演算子
# Good
package?stats
?mean

# Bad
package ? stats
? mean

余分な空白

=または<-の配置が改善される場合は、スペースを追加しても問題はない。

# Good
list(
  total = a + b + c,
  mean  = (a + b + c) / n
)

# Also fine
list(
  total = a + b + c,
  mean = (a + b + c) / n
)

関数呼び出し

名前付き引数

関数の引数は通常2つの大きなカテゴリに分類される。1つは計算するデータを与える。もう1つは計算の詳細を管理する。関数を呼び出すとき、データ引数の名前は非常に一般的に使用されるため、通常は省略する。引数のデフォルト値を上書きする場合は、フルネームを使用する。

# Good
mean(1:10, na.rm = TRUE)

# Bad
mean(x = 1:10, , FALSE)
mean(, TRUE, x = c(1:10, NA))

部分的な一致は避ける。

割り当て

関数呼び出しでの割り当ては避ける

# Good
x <- complicated_function()
if (nzchar(x) < 1) {
  # do something
}

# Bad
if (nzchar(x <- complicated_function()) < 1) {
  # do something
}

唯一の例外は、副作用をキャプチャする関数

output <- capture.output(x <- f())

制御フロー

コードブロック

中括弧{}はRコードの中でも重要な階層を定義する。この階層を見やすくするには以下のルールが必要。

  • {は行の最後の文字である。関連するコード(if句、関数宣言、末尾のコンマなど)は初めの中括弧と同じ行にある。
  • 内容は2行のスペースでインデントする。
  • }は行の最初の文字である。
# Good
if (y < 0 && debug) {
  message("y is negative")
}

if (y == 0) {
  if (x > 0) {
    log(x)
  } else {
    message("x is negative or zero")
  }
} else {
  y^x
}

test_that("call1 returns an ordered factor", {
  expect_s3_class(call1(x, y), c("factor", "ordered"))
})

tryCatch(
  {
    x <- scan()
    cat("Total: ", sum(x), "\n", sep = "")
  },
  interrupt = function(e) {
    message("Aborted by user")
  }
)

# Bad
if (y < 0 && debug) {
message("Y is negative")
}

if (y == 0)
{
    if (x > 0) {
      log(x)
    } else {
  message("x is negative or zero")
    }
} else { y ^ x }

インラインステートメント

副作用がない限り、1行に収まる非常に単純な記述は中括弧を省略してもよい。

# Good
y <- 10
x <- if (y < 20) "Too low" else "Too high"

return()stop()continue)のような制御フローに影響を与える関数呼び出しについては常に{}ブロック内に置く。

# Good
if (y < 0) {
  stop("Y is negative")
}

find_abs <- function(x) {
  if (x > 0) {
    return(x)
  }
  x * -1
}

# Bad
if (y < 0) stop("Y is negative")

if (y < 0)
  stop("Y is negative")

find_abs <- function(x) {
  if (x > 0) return(x)
  x * -1
}

暗黙的な型強制

ifステートメントでの暗黙的な型強制(数値から論理など)は避ける。

# Good
if (length(x) > 0) {
  # do something
}

# Bad
if (length(x)) {
  # do something
}

Switchステートメント

  • 位置ベースのswitch()ステートメントは避ける(つまり名前を優先する)
  • 各要素は独自の行に配置する
  • 次の要素に分類される要素には=の後にスペースを入れる。
  • 以前に入力を検証していない限り、フォールスルーエラーを提示する。
# Good 
switch(x
  a = ,
  b = 1, 
  c = 2,
  stop("Unknown `x`", call. = FALSE)
)

# Bad
switch(x, a = , b = 1, c = 2)
switch(x, a =, b = 1, c = 2)
switch(y, 1, 2, 3)i

長い行

コードを1行あたり80文字に制限する。関数呼び出しが長すぎて1行に収まらない場合は、関数名、仮引数、および最後の)にそれぞれ1行使用する。

# Good
do_something_very_complicated(
  something = "that",
  requires = many,
  arguments = "some of which may be long"
)

# Bad
do_something_very_complicated("that", requires, many, arguments,
                              "some of which may be long"
                              )

[名前付き引数]でも説明されているように、非常に一般的な引数(関数のほとんどすべての呼び出しで使用される引数)の引数名は省略できる。関数呼び出し全体が複数行にまたがる場合でも、名前のない短い引数は関数名と同じ行に配置することもできる。

map(x, f,
  extra_argument_a = 10,
  extra_argument_b = c(1, 43, 390, 210209)
)

paste()stop()といった文字列呼び出し関数内では、互いに密接に関連している場合は、同じ行に複数の引数を配置できる。文字列を作成するときは、可能であれば、1行のコードを1行の出力に一致させる。

# Good
paste0(
  "Requirement: ", requires, "\n",
  "Result: ", result, "\n"
)

# Bad
paste0(
  "Requirement: ", requires,
  "\n", "Result: ",
  result, "\n")

セミコロン

行の最後に;を入れない。また1行に複数のコマンドを置くために;を使用しない

割り当て

割り当てには=ではなく <-を使用する。

データ

文字列ベクトル

テキストの引用には'ではなく"を使用する。唯一の例外は既に二重引用符が含まれていて、一重引用符が含まれていない場合である。

# Good
"Text"
'Text with "quotes"'
'<a href="http://style.tidyverse.org">A link</a>'

# Bad
'Text'
'Text with "double" and \'single\' quotes'

論理ベクトル

TFよりもTRUEFALSEを優先する。

コメント

コメントの各行は、コメント記号#と1つのスペースで始まる必要がある。
データ分析コードでは、コメントを使用して重要な調査結果と分析の決定を記録する。コードの動作を説明するコメントが必要な場合は、コードをより明確に書き直すことを検討する。コードよりもコメントが多いことが分かった場合は、RMarkdownに切り替えることを検討する。

関数

名前

関数名には動詞を使用するようにする。

# Good
add_row()
permute()

# Bad
row_adder()
permutation()

長い行

関数定義が複数行にまたがる場合は、定義が始まる場所まで2行目をインデントする。

# Good
long_function_name <- function(a = "a long argument",
                               b = "another argument",
                               c = "another long argument") {
  # As usual code is indented by two spaces.
}

# Bad
long_function_name <- function(a = "a long argument",
  b = "another argument",
  c = "another long argument") {
  # Here it's hard to spot where the definition ends and the
  # code begins
}

return()

早期返却の時のみreturn()を使用し、それ以外の場合はRに頼り、最後に評価された式の結果を返す。

# Good
find_abs <- function(x) {
  if (x > 0) {
    return(x)
  }
  x * -1
}
add_two <- function(x, y) {
  x + y
}

# Bad
add_two <- function(x, y) {
  return(x + y)
}

戻りステートメントは、制御フローに重要な影響を与えるため、常に独自の行に配置する必要がある。

# Good
find_abs <- function(x) {
  if (x > 0) {
    return(x)
  }
  x * -1
}

# Bad
find_abs <- function(x) {
  if (x > 0) return(x)
  x * -1
}

関数が主にその副作用(プリント、プロット、ディスク保存など)のために呼び出された場合、最初の引数を目に見えない形で返す必要がある。これにより、パイプの一部として機能を使用することが可能になる。printメソッドは通常httrの例のようにこれを実行する必要がある。

print.url <- function(x, ...) {
  cat("Url: ", build_url(x), "\n", sep = "")
  invisible(x)
}

コメント

コードでは、コメントを使用して、「何を」または「どのように」ではなく、「なぜ」を説明する。コメントの各行は、コメント記号#と1つのスペースで始まる必要がある。

# Good

# Objects like data frames are treated as leaves
x <- map_if(x, is_bare_list, recurse)


# Bad

# Recurse only with bare lists
x <- map_if(x, is_bare_list, recurse)

コメントは文の場合、少なくとも2つの文が含まれている場合にのみピリオドで終了する必要がある。

# Good

# Objects like data frames are treated as leaves
x <- map_if(x, is_bare_list, recurse)

# Do not use `is.list()`. Objects like data frames must be treated
# as leaves.
x <- map_if(x, is_bare_list, recurse)


# Bad

# objects like data frames are treated as leaves
x <- map_if(x, is_bare_list, recurse)

# Objects like data frames are treated as leaves.
x <- map_if(x, is_bare_list, recurse)

パイプ

はじめに

アクションが実行されているオブジェクトではなく、一連のアクションを強調するために%>%を使用する。
次の場合はパイプの使用を避ける。

  • 一度に複数のオブジェクトを操作する必要がある。1つのプライマリオブジェクトに適用される一連のステップ用にパイプを取っておく。
  • 有益な名前を付けることができる意味のある中間オブジェクトがある。

空白

%>%の前には常にスペースが必要であり、通常はその後に改行が続く必要がある。最初のステップの後、各行を2つのスペースでインデントする必要がある。この構造により、新しいステップの追加または既存のステップの再配置が容易になり、ステップを見落としにくくなる。

# Good
iris %>%
  group_by(Species) %>%
  summarize_if(is.numeric, mean) %>%
  ungroup() %>%
  gather(measure, value, -Species) %>%
  arrange(value)

# Bad
iris %>% group_by(Species) %>% summarize_all(mean) %>%
ungroup %>% gather(measure, value, -Species) %>%
arrange(value)

長い行

関数の引数がすべての行に収まらない場合は、各引数の独自の行に配置してインデントする。

iris %>%
  group_by(Species) %>%
  summarise(
    Sepal.Length = mean(Sepal.Length),
    Sepal.Width = mean(Sepal.Width),
    Species = n_distinct(Species)
  )

短いパイプ

ワンステップパイプは1行に留めることができるが、後で拡張する予定がない限り、通常の関数呼び出しに書き直すことを検討する必要がある。

# Good
iris %>% arrange(Species)

iris %>% 
  arrange(Species)

arrange(iris, Species)

長いパイプの関数の引数として、短いパイプを含めると便利な場合がある。短いインラインパイプ(他の場所で調べる必要がない)でコードが読みやすくなるかどうか、または、コードをパイプの外に移動して喚起的な名前を付けるほうが良いかは慎重に検討する。

# Good
x %>%
  select(a, b, w) %>%
  left_join(y %>% select(a, b, v), by = c("a", "b"))

# Better
x_join <- x %>% select(a, b, w)
y_join <- y %>% select(a, b, v)
left_join(x_join, y_join, by = c("a", "b"))

引数なし

magrittrを使用すると引数を持たない関数の()を省略できるが、この機能は避ける。

割り当て

割り当てには3つの受け入れ可能な形式がある。

  • 変数名と個別の行の割り当て:
    iris_long <-
      iris %>%
      gather(measure, value, -Species) %>%
      arrange(-value)
    
  • 同じ行の変数名と割り当て:
    iris_long <- iris %>%
      gather(measure, value, -Species) %>%
      arrange(-value)
    
  • パイプの端での割り当て:
    iris %>%
      gather(measure, value, -Species) %>%
      arrange(-value) ->
      iris_long
    
    これは書き方としては自然だが、読むのが少し難しくなる。名前が最初に来ると、パイプの目的を思い出させるための見出しとして機能する可能性がある。

magrittrパッケージは、オブジェクトを適切に変更するためのショートカットとして%<>%オブジェクトを提供していが、この演算子は避ける。

# Good
x <- x %>% 
  abs() %>% 
  sort()
  
# Bad
x %<>%
  abs() %>% 
  sort()

ggplot2

はじめに

ggplot2レイヤーを分離するために+が使用されるスタイリングの提案は、パイプラインの提案と非常によく似ている。

空白

+の前には常にスペースが必要であり、その後には改行が続く必要がある。これは、プロットに2つのレイヤーしかない場合でも当てはまる。最初のステップの後、各行を2つのスペースでインデントする必要がある。
dplyrパイプラインからggplotを作成する場合は、1レベルのインデントのみ必要

# Good
iris %>%
  filter(Species == "setosa") %>%
  ggplot(aes(x = Sepal.Width, y = Sepal.Length)) +
  geom_point()

# Bad
iris %>%
  filter(Species == "setosa") %>%
  ggplot(aes(x = Sepal.Width, y = Sepal.Length)) +
    geom_point()

# Bad
iris %>%
  filter(Species == "setosa") %>%
  ggplot(aes(x = Sepal.Width, y = Sepal.Length)) + geom_point()

長い行

ggplot2レイヤーへの引数がすべて1行に収まらない場合は、各引数を独自の行に配置してインデントする。

# Good
ggplot(aes(x = Sepal.Width, y = Sepal.Length, color = Species)) +
  geom_point() +
  labs(
    x = "Sepal width, in cm",
    y = "Sepal length, in cm",
    title = "Sepal length vs. width of irises"
  ) 

# Bad
ggplot(aes(x = Sepal.Width, y = Sepal.Length, color = Species)) +
  geom_point() +
  labs(x = "Sepal width, in cm", y = "Sepal length, in cm", title = "Sepal length vs. width of irises") 

ggplot2を使用すると、data引数内でフィルタリングやスライスなどのデータ操作を行うことができる。だが、これは避け、代わりにプロットを開始する前にパイプラインでデータ操作を行う。

# Good
iris %>%
  filter(Species == "setosa") %>%
  ggplot(aes(x = Sepal.Width, y = Sepal.Length)) +
  geom_point()

# Bad
ggplot(filter(iris, Species == "setosa"), aes(x = Sepal.Width, y = Sepal.Length)) +
  geom_point()

ファイル

第1章のアドバイスの部分は、パッケージ内のファイルにも当てはまる。重要な違いをいかに説明する。

名前

  • ファイルに単一の関数が含まれている場合は、ファイルに関数と同じ名前を付ける。
  • ファイルに複数の関連する機能が含まれている場合は、簡潔で分かりやすい名前を付ける。
  • 非推奨の関数は、deprecプレフィックス付きのファイルに存在する必要がある。
  • 互換性関数は、compatプレフィックス付きのファイルに存在する必要がある。

編成

複数の関数を含むファイルでは、パブリック関数とそのドキュメントが最初に表示され、プライベート関数はすべてのドキュメント化された関数の後に表示される。複数のパブリック関数が同じドキュメントを共有する場合、それらはすべてドキュメントブロックの直後に続く必要がある。

パッケージ内の関数の文書化に関する詳細なガイダンスは次章を参照せよ。

# Bad
help_compute <- function() {
 # ... Lots of code ...
}

#' My public function
#'
#' This is where the documentation of my function begins.
#' ...
#' @export
do_something_cool <- function() {
 # ... even more code ...
 help_compute()
}
# Good
#' Lots of functions for doing something cool
#'
#' ... Complete documentation ...
#' @name something-cool
NULL

#' @describeIn something-cool Get the mean
#' @export
get_cool_mean <- function(x) {
 # ...
}

#' @describeIn something-cool Get the sum
#' @export
get_cool_sum <- function(x) {
 # ...
}

ドキュメント

はじめに

あなたのコードを使用しているのが未来だとしても、コードの文書化は不可欠である。ドキュメントをコードに近づけるために、マークダウンサポートを有効にしてroxygen2を使用する。

タイトルと説明

関数ドキュメントの最初の行を使用して、関数、データセット、またはクラスを説明する簡潔なタイトルを付ける。タイトルは分の大文字小文字を使用する必要があるが、ピリオドで終わらせない。

#' Combine values into a vector or list
#' 
#' This is a generic function which combines its arguments.
#'

複数の段落であるか、箇条書きのような複雑なフォーマットが含まれている場合を除いて、明示的な@titleまたは@descriptionタグを使用する必要はない。

#' Apply a function to each element of a vector
#'
#' @description
#' The map function transform the input, returning a vector the same length
#' as the input. 
#' 
#' * `map()` returns a list or a data frame
#' * `map_lgl()`, `map_int()`, `map_dbl()` and `map_chr()` return 
#'     vectors of the corresponding type (or die trying); 
#' * `map_dfr()` and `map_dfc()` return data frames created by row-binding 
#'    and column-binding respectively. They require dplyr to be installed.

インデントと改行

#'の後には常に1つのスペースでインデントする。roxygenタグに対応する説明が複数行にまたがる場合は、さらに2つのスペースを追加してインデントを追加する。

#' @param key The bare (unquoted) name of the column whose values will be used 
#'   as column headings. 

また、(@description@examples@section)のような複数行にわたるタグは対応するタグを独自の行に含めることができ、後続の行はインデントする必要はない。

#' @examples
#' 1 + 1
#' sin(pi)

必要に応じて、セクションの前後に開業を使用する。

#' @section Tidy data:
#' When applied to a data frame, row names are silently dropped. To preserve,
#' convert to an explicit variable with [tibble::rownames_to_column()].
#'
#' @section Scoped filtering:
#' The three [scoped] variants ([filter_all()], [filter_if()] and
#' [filter_at()]) make it easy to apply a filtering condition to a
#' selection of variables.

パラメータの文書化

@param@seealso@return)のようなほとんどのタグでは、テキストは大文字で始まり、ピリオドで終わる分である必要がある。

#' @param key The bare (unquoted) name of the column whose values will be used 
#'   as column headings. 

一部の関数がパラメータを共有している場合@inheritParamsは、複数の場所でのコンテンツの重複を回避するために使用できる。

#' @inheritParams function_to_inherit_from

大文字と終止符

すべての箇条書き、列挙、引数の説明などでは、たとえ数語であっても、文の大文字と小文字を区別し、各テキスト要素の最後にピリオドを付ける。ただし、Rでは大文字と小文字が区別されるため、関数名またはパッケージの大文字化は避ける。列挙または箇条書きの前にコロンを使用する。

#' @details 
#' In the following, we present the bullets of the list:
#' * Four cats are few animals.
#' * forcats is a package.

クロスリンク

Rのヘルプファイルシステム内と外部リソースの両方で、相互参照を推奨する。密接に関連する関数を@seealsoにリスト化する。関連する単一の関数を文として記述できる。

#' @seealso [fct_lump()] to automatically convert the rarest (or most common)
#'   levels to "other".

その他の推奨事項は箇条書きにまとめる必要がある。

#' @seealso
#' * [tibble()] constructs from individual columns.
#' * [enframe()] converts a named vector into a two-column tibble (names and 
#'   values).
#' * [name-repair] documents the details of name repair.

関連する関数のファミリーがある場合は、@familyタグを使用して、適切なリストとインターリンクを@seealsoセクションに自動的に追加できる。姓は複数形である。dplyrでは、動詞arrange()filter()mutate()slice()summarize()は単一のテーブル動詞のファミリーを形成する。

#' @family single table verbs

外部リソースにリンクする場合は、<>にインラインで完全なURLを含めるか、周囲の散文とリンクテキストで、ハイパーリンクの行き先を非常に明確にする必要がある。「ここをクリック」のようなテキストは避ける。

Rコード

有効なRコードを含むテキストは、バッククォートを使用してそのようにマークする必要がある。

  • 関数名の後に()。例えばtibble()
  • 関数の引数。例:na.rm
  • 値。例えば、TRUEFALSENANaN...NULL
  • リテラルRコード。例:mean(x,na.rm = TRUE)
  • クラス名、例えば「tibbleはクラスを持つtbl_df

パッケージ名にコードフォントを使用しない。パッケージ名が文脈上あいまいな場合は、「fooパッケージ」などの単語で明確にする。文の先頭にある場合は、関数名を大文字にしない。

内部機能

内部機能は通常通り#'コメント付きで文書化する必要がある。@noRdタグを使用して、.Rdファイルが生成されないようにする。

#' Drop last
#'
#' Drops the last element from a vector.
#'
#' @param x A vector object to be trimmed.
#'
#' @noRd

テスト

編成

テストファイルの編成はR/ファイルと一致する必要がある。関数がR/foofy.Rに存在する場合、テストはtests/testthat/test-foofy.Rに存在する必要がある。
usethis::use_test()は正しい名前のファイルを自動的に作成するために使用する。
context()はそれほど重要ではない。出力にコンテキストの代わりにファイル名を表示するtestthatの将来のバージョン。

エラーメッセージ

エラーメッセージは、問題の一般的な説明で始まり、何が悪かったのかを簡潔に説明する必要がある。句読点と書式設定を一貫して使用すると、エラーの解析が容易になる。
(このガイドは現在、ほぼ完全に野心的です。悪い例のほとんどは、既存のtidyverseコードからのものである。)

問題の説明

すべてのエラーメッセージは、問題の一般的な説明で始まる必要がある。簡潔である必要がありますが、有益である。
できるだけ有益であることが推奨されるが、ローカリゼーションと翻訳を可能にするため、各文は非常に単純である必要がある。 A Localization Horror Story: It Could Happen To Youはエラーメッセージをローカライズする課題についての良い要約である、現在ローカライズされたメッセージをサポートしていない可能性があるが、将来的にはできるだけ簡単にサポートできるようにする必要がある。
理想的には、各文には1つのフレーズが含まれ、1つの可変量のみに言及する必要がある。複雑な文を避けるため、箇条書きに情報を配置することを好む。コンテキスト情報のリストから始めて、誤ったユーザー入力に関する情報のリストで終わる。これらのリストには、UTF-8が使用可能な場合はそれぞれℹと✖(色が使用可能な場合は青と赤)、それ以外の場合はASCII *文字をプレフィックスとして付ける必要がある。

  • 問題の原因がわかる場合には「must」を使用する。
    dplyr::nth(1:10, "x")
    #> Error: `n` must be a numeric vector:
    #> ✖ You've supplied a character vector.
    
    dplyr::nth(1:10, 1:2)
    #> Error: `n` must have length 1
    #> ✖ You've supplied a vector of length 2.
    
    明確な切断の原因には通常タイプや長さが正しくない。
  • 期待したことを記述することができない場合は、「can't」を使用する。
    mtcars %>% pull(b)
    #> Error: Can't find column `b` in `.data`.
    
    as_vector(environment())
    #> Error: Can't coerce `.x` to a vector.
    
    purrr::modify_depth(list(list(x = 1)), 3, ~ . + 1)
    #> Error: Can't find specified `.depth` in `.x`.
     
    

問題の説明では、文の大文字小文字を区別し、終止符で終了する必要がある。
stop(call. = FALSE)rlang::abort()Rf_errorcall(R_NilValue, ...)を使用し、それを生成した関数の名前とエラーメッセージの混乱を回避する。その情報は多くの場合有益ではなく、traceback()または同等のIDEを介して簡単にアクセスできる。
ℹおよび✖要素の箇条書きリストに配置された簡単な文を使用する。

  • 文は短く、箇条書きにする必要がある。
# Good
vec_slice(letters, 100)
#> Must index an existing element:
#> ℹ There are 26 elements.
#> ✖ You've tried to subset element 100.

# Bad
vec_slice(letters, 100)
#> Must index an existing element.
#> There are 26 elements and you've tried to subset element 100.
  • コンテキスト情報を最初にする必要がある
    # Good
    vec_slice(letters, 100)
    #> Must index an existing element:
    #> ℹ There are 26 elements.
    #> ✖ You've tried to subset element 100.
    
    # Bad
    vec_slice(letters, 100)
    #> Must index an existing element:
    #> ✖ You've tried to subset element 100.
    #> ℹ There are 26 elements.
    

エラーの場所

面倒なコンポーネントの場所、名前、コンテンツを明らかにするために最善を尽くす。目標は、ユーザーが問題を見つけて修正するのをできるだけ簡単にすることである。

# Good
map_int(1:5, ~ "x")
#> Error: Each result must be a single integer:
#> ✖ Result 1 is a character vector.

# Bad
map_int(1:5, ~ "x")
#> Error: Each result must be a single integer

(正確な問題を特定するのは簡単ではないことがよくある。下位レベルで生成されたエラーメッセージが元のソースを認識できるように、追加の引数を渡す必要がある場合がある。頻繁に使用される関数の場合、通常は努力する価値がある。)
エラーの原因が不明な場合は、エラーの原因について意見を述べて、ユーザーを間違った方向に向けないようにする。

# Good
pull(mtcars, b)
#> Error: Can't find column `b` in `.data`.

tibble(x = 1:2, y = 1:3, z = 1)
#> Error: Columns must have consistent lengths: 
#> ✖ Column `x` has length 2
#> ✖ Column `y` has length 3

# Bad: implies one argument at fault
pull(mtcars, b)
#> Error: Column `b` must exist in `.data`

pull(mtcars, b)
#> Error: `.data` must contain column `b`

tibble(x = 1:2, y = 1:3, z = 1)
#> Error: Column `x` must be length 1 or 3, not 2 

複数の問題がある場合、またはいくつかの議論や項目にわたって矛盾が明らかになった場合は、箇条書きをお勧めする。

# Good
purrr::reduce2(1:4, 1:2, `+`)
#> Error: `.x` and `.y` must have compatible lengths:
#> ✖ `.x` has length 4
#> ✖ `.y` has length 2

# Bad: harder to scan
purrr::reduce2(1:4, 1:2, `+`)
#> Error: `.x` and `.y` must have compatible lengths: `.x` has length 4 and 
#> `.y` has length 2

問題のリストが長い可能性がある場合は、最初のいくつかだけを表示するように切り捨てる。

# Good
#> Error: NAs found at 1,000,000 locations: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...

エラーメッセージを正しく複数形にする場合は、ngettext()の使用を検討する。他の言語への正しい翻訳に関連するいくつかの課題については、?ngettext()の注記を参照せよ。

ヒント

エラーの原因が明確で一般的である場合は、それを修正する方法についてのヒントを提供することをお勧めする。UTF-8が使用可能な場合は、プレフィックスℹ(色が使用可能な場合は青)を付ける。

dplyr::filter(iris, Species = "setosa")
#> Error: Filter specifications must be named.
#> ℹ Did you mean `Species == "setosa"`?

ggplot2::ggplot(ggplot2::aes())
#> Error: Can't plot data with class "uneval". 
#> ℹ Did you accidentally provide the results of aes() to the `data` argument?

ヒントは常に疑問符で終わる必要がある。
エラーの原因が根本原因から遠く離れている場合、ヒントは特に重要ある。

# Bad
mean[[1]]
#> Error in mean[[1]] : object of type 'closure' is not subsettable

# BETTER
mean[[1]]
#> Error: Can't subset a function.

# BEST
mean[[1]]
#> Error: Can't subset a function.
#> ℹ Have you forgotten to define a variable named `mean`?

上記のように、ユーザーを間違った方向に誘導することを避けたいので、良いヒントを書くのは難しい。 一般的に、問題が一般的でない限り、ヒントを書くことを避ける、(例えばStackOverflowを検索することによって)間違った一般的なパターンを簡単に見つけることができる 。

句読点

  • エラーは文の大文字小文字で記述し、終止符で終了する必要がある。箇条書きも同様にフォーマットする必要がある。必ず最初の単語を大文字にする(引数または列名でない限り)。
  • 問題の記述は単数を優先する。
    # Good
    map_int(1:2, ~ "a")
    #> Error: Each result must be coercible to a single integer:
    #> ✖ Result 1 is a character vector.
    
    # Bad
    map_int(1:2, ~ "a")
    #> Error: Results must be coercible to single integers: 
    #> ✖ Result 1 is a character vector
    
  • 複数の問題を検出できる場合は、最大5つまでリストにする。これにより、ユーザーは、同じソースを持つ可能性のある多くのエラーに圧倒されることなく、1回のパスで複数の問題を修正できる。
    # BETTER
    map_int(1:10, ~ "a")
    #> Error: Each result must be coercible to a single integer:
    #> ✖ Result 1 is a character vector
    #> ✖ Result 2 is a character vector
    #> ✖ Result 3 is a character vector
    #> ✖ Result 4 is a character vector
    #> ✖ Result 5 is a character vector
    #> ... and 5 more problems
    
  • 問題の説明とエラーの場所の間の自然なコネクタを選択する。これは、コンテキストに応じて、「,not」 「,」 「;」または「:」の場合がある。
  • 引数の名前をバッククォートで囲む(例x)。「column」を使用して、列と引数を明確にする(Column x)。「variable」はあいまいなので避ける。
  • 理想的には、エラーメッセージの各コンポーネントの幅は80文字未満である必要がある。長いエラーメッセージに手動の改行を追加しない。コンソールが予想よりも狭い(またははるかに広い)場合、正しく表示されない。代わりに、箇条書きを使用してエラーをより短い論理コンポーネントに分割する。

以前と以後

tidyverseの周りからさらに多くの例が集められた。

dplyr::filter(mtcars, cyl)
#> BEFORE: Argument 2 filter condition does not evaluate to a logical vector 
#> AFTER:  Each argument must be a logical vector:
#> * Argument 2 (`cyl`) is an integer vector.

tibble::tribble("x", "y")
#> BEFORE: Expected at least one column name; e.g. `~name` 
#> AFTER:  Must supply at least one column name, e.g. `~name`.

ggplot2::ggplot(data = diamonds) + ggplot2::geom_line(ggplot2::aes(x = cut))
#> BEFORE: geom_line requires the following missing aesthetics: y
#> AFTER:  `geom_line()` must have the following aesthetics: `y`.

dplyr::rename(mtcars, cyl = xxx)
#> BEFORE: `xxx` contains unknown variables
#> AFTER:  Can't find column `xxx` in `.data`.

dplyr::arrange(mtcars, xxx)
#> BEFORE: Evaluation error: object 'xxx' not found.
#> AFTER:  Can't find column `xxx` in `.data`.

ニュース

パッケージに対するユーザー向けの各変更には、NEWS.mdの箇条書きを付ける必要がある。ドキュメントへの小さな変更を文書化する必要はないが、抜本的な変更と新しいビネットに注意を引くことは価値がある。

箇条書き

箇条書きの目的は、パッケージのユーザーが何が変更されたかを理解できるように、変更について簡単に説明することである。これはコミットメッセージに似ているが、(開発者ではなく)ユーザーを念頭に置いて書かれている。
新しい箇条書きをファイルの先頭(最初の見出しの下)に追加する必要がある。箇条書きの編成は、後でリリースプロセス中に行われる。

一般的なスタイル

関数の名前を箇条書きの先頭にできるだけ近づけるようにする。一貫した場所により、箇条書きのスキャンが容易になり、リリース前の整理が容易になる。

# Good
* `ggsave()` now uses full argument names to avoid partial match warning (#2355).

# Bad
* Fixed partial argument matches in `ggsave()` (#2355).

行は80文字に折り返され、各箇条書きは終止符で終わる必要がある。
箇条書きを積極的に組み立て、現在形を使用する(つまり、以前の出来事ではなく、現在の出来事)。

# Good
* `ggsave()` now uses full argument names to avoid partial match warnings (#2355).

# Bad
* `ggsave()` no longer partially matches argument names (#2355).

多くのニュースの箇条書きは一文になる。これは通常、バグ修正やマイナーな改善を説明する場合には適切だが、新機能を説明する場合はさらに詳細が必要になる場合がある。より複雑な機能については、フェンスで囲まれたコードブロック(```)に長い例を含める。これらは、後でブログ投稿を書くときに役立つインスピレーションになる。

# Good
* In `stat_bin()`, `binwidth` now also takes functions.

# Better
* In `stat_bin()`, `binwidth` now also takes functions. The function is 
  called with the scaled `x` values, and should return a single number.
  This makes it possible to use classical binwidth computations with ggplot2.

# Best
* In `stat_bin()`, `binwidth` now also takes functions. The function is 
  called with the scaled `x` values, and should return a single number.
  With a little work, this makes it possible to use classical bin size 
  computations with ggplot2.
  
  ```R
  sturges <- function(x) {
    rng <- range(x)
    bins <- nclass.Sturges(x)
    
    (rng[2] - rng[1]) / bins
  }
  ggplot(diamonds, aes(price)) +
    geom_histogram(binwidth = sturges) + 
    facet_wrap(~cut)
  `` `

謝辞

箇条書きが問題に関連している場合は、問題番号を含める。投稿がPRであり、作成者がパッケージの作成者でない場合は、GitHubユーザー名を含める。両方のアイテムは括弧で囲む必要があり、通常は最終ピリオドの前に来る。

# Good
* `ggsave()` now uses full argument names to avoid partial match warnings 
  (@wch, #2355).

# Bad
* `ggsave()` now uses full argument names to avoid partial match warnings.

* `ggsave()` now uses full argument names to avoid partial match warnings.
  (@wch, #2355)

コードスタイル

関数、引数、およびファイル名はバッククォートで囲む必要がある。関数名には括弧を含める必要がある。「the argument」または「the function」を省略する。

# Good
* In `stat_bin()`, `binwidth` now also takes functions.

# Bad
* In the stat_bin function, "binwidth" now also takes functions.

一般的なパターン

整頓されたニュースエントリからの以下の抜粋は、従うのに役立つテンプレートを提供する。

  • 新しい機能ファミリー:
    * Support for ordered factors is improved. Ordered factors throw a warning 
      when mapped to shape (unordered factors do not), and do not throw warnings 
      when mapped to size or alpha (unordered factors do). Viridis is used as 
      default colour and fill scale for ordered factors (@karawoo, #1526).
    
    * `possibly()`, `safely()` and friends no longer capture interrupts: this
      means that you can now terminate a mapper using one of these with
      Escape or Ctrl + C (#314).
    
  • 新機能:
    * New `position_dodge2()` provides enhanced dogding for boxplots...
    
    * New `stat_qq_line()` makes it easy to add a simple line to a Q-Q plot. 
      This line makes it easier to judge the fit of the theoretical distribution 
      (@nicksolomon).
    
  • 既存の関数への新しい引数:
    * `geom_segment()` gains a `linejoin` parameter.
    
  • 関数の引数は動作を変更する:
    * In `separate()`, `col = -1` now refers to the far right position. 
      Previously, and incorrectly, `col = -2` referred to the far-right 
      position.
    
  • 関数は動作を変更する
    * `map()` and `modify()` now work with calls and pairlists (#412).
    
    * `flatten_dfr()` and `flatten_dfc()` now aborts with informative 
       message if dplyr is not installed (#454).
    
    * `reduce()` now throws an error if `.x` is empty and `.init` is not
      supplied.
    

編成

開発

開発中は、ファイルの先頭の「development」見出しのすぐ下に新しい箇条書きを追加する必要がある。

# haven (development version)

* Second update.

* First update.

リリース

リリース前に、NEWSファイルを徹底的に校正して手入れをする必要がある。

各リリースには、パッケージ名とバージョン番号を含むレベル1の見出し(#)が必要である。小さいパッケージまたはパッチリリースの場合、この量の編成で十分な場合がある。たとえば、モデラー0.1.2のニュースは次のとおりである。

# modelr 0.1.2

* `data_grid()` no longer fails with modern tidyr (#58).

* New `mape()` and `rsae()` model quality statistics (@paulponcet, #33).

* `rsquare()` use more robust calculation 1 - SS_res / SS_tot rather 
  than SS_reg / SS_tot (#37).

* `typical()` gains `ordered` and `integer` methods (@jrnold, #44), 
  and `...` argument (@jrnold, #42).

箇条書きが多い場合は、バージョンの見出しの後に、レベル2の見出し(##)を持つ関連領域にグループ化された問題を続ける必要がある。一般的に使用される3つのセクションを以下に示す。

# package 1.1.0

## Breaking changes

## New features

## Minor improvements and fixes

別の編成が理にかなっている場合は、これらの見出しから逸脱しても問題ない。実際、パッケージが大きいほど、より細かい内訳が必要になることがよくある。たとえば、ggplot22.3.0には次の見出しが含まれていた。

# ggplot 2.3.0
## Breaking changes
## New features
### Tidy evaluation
### sf
### Layers: geoms, stats, and position adjustments
### Scales and guides
### Margins
## Extension points
## Minor bug fixes and improvements
### Facetting
### Scales
### Layers
### Coords
### Themes
### Guides
### Other

グループが事前に何になるかは通常明らかではないため、開発中に箇条書きを見出しに整理することは価値がない。
セクション内では、箇条書きは最初に述べた機能のアルファベット順に並べる必要がある。機能が記載されていない場合は、セクションの上部に箇条書きを配置する。

重大な変更

(revdepchecks中に発見された)APIを壊す変更がある場合、それらは上部の独自のセクションにも表示される。各箇条書きには、変更の症状の説明と、それを修正するために必要なものを含める必要がありる。箇条書きは、適切なセクションでも繰り返す必要がある。

## Breaking changes

* `separate()` now correctly uses -1 to refer to the far right position, 
  instead of -2. If you depended on this behaviour, you'll need to condition
  on `packageVersion("tidyr") > "0.7.2"`.

ブログ投稿

すべてのメジャーリリースとマイナーリリースについて、最新ニュースをブログ投稿に変換する必要がある。ブログ投稿では、ユーザー向けの主要な変更点を強調し、詳細についてはリリースノートを参照する必要がある。一般に、新機能の動作を示す例を含め、新機能と主要な改善に焦点を当てる必要がある。やる気のある読者はリリースノートでこれらを見つけることができるので、マイナーな改善やバグ修正について説明する必要はない。

Git/GitHub

コミットメッセージ

standard git commit message adviceに従う。簡単にいうと:

  • 最初の行は件名であり、コミットの変更を50文字未満で要約する必要がある。
  • 追加の詳細が必要な場合は、空白行を追加してから、説明とコンテキストを段落形式で提供する。
  • コミットでGitHubの問題が修正された場合は、Fixes #<issue-number>を含める。これにより、コミットがマスターにマージされたときに問題が自動的にクローズされる。

プルリクエスト

プルリクエストのタイトルには、加えられた変更を簡単に説明する必要がある。タイトルは無類である必要があり、関連する問題番号を含めない。(つまり、Fixes #10を書かない)。
非常に単純な変更の場合、差分を見れば明らかなことを説明する必要がないため、説明を空白のままにすることができる。より複雑な変更については、変更の概要を説明する必要がある。PRで問題が修正された場合は、必ずFixes #<issue-number>を説明に含める。

Discussion