diff --git a/functions.qmd b/functions.qmd index e8812bc..6a60524 100755 --- a/functions.qmd +++ b/functions.qmd @@ -154,7 +154,7 @@ add_two <- function(x, y) { } ``` -Return statements should always be on their own line because they have important effects on the control flow. See also [inline statements](#inline-statements). +Return statements should always be on their own line because they have important effects on the control flow. See also [control flow modifiers](#control-flow-modifiers). ```{r} # Good diff --git a/syntax.qmd b/syntax.qmd index 73aa6d7..3cde2cc 100755 --- a/syntax.qmd +++ b/syntax.qmd @@ -292,24 +292,85 @@ The only exception is in functions that capture side-effects: output <- capture.output(x <- f()) ``` +### Long function calls -## Control flow +Strive to limit your code to 80 characters per line. This fits comfortably on a +printed page with a reasonably sized font. If you find yourself running out of +room, this is a good indication that you should encapsulate some of the work in +a separate function or use early returns to reduce the nesting in your code. + +If a function call is too long to fit on a single line, use one line each for +the function name, each argument, and the closing `)`. +This makes the code easier to read and to change later. + +```{r} +# 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" + ) +``` + +As described under [Named arguments](#argument-names), you can omit the argument names +for very common arguments (i.e. for arguments that are used in almost every +invocation of the function). If this introduces a large disparity between the line lengths, you may want to supply names anyway: + +```{r} +# Good +my_function( + x, + long_argument_name, + extra_argument_a = 10, + extra_argument_b = c(1, 43, 390, 210209) +) + +# Also good +my_function( + x = x, + y = long_argument_name, + extra_argument_a = 10, + extra_argument_b = c(1, 43, 390, 210209) +) +``` + +You may place multiple unnamed arguments on the same line if they are closely +related to each other. A common example of this is creating strings +with `paste()`. In such cases, it's often beneficial to match one line of code +to one line of output. -### Code blocks {#indenting} +```{r} +# Good +paste0( + "Requirement: ", requires, "\n", + "Result: ", result, "\n" +) -Curly braces, `{}`, define the most important hierarchy of R code. To make this -hierarchy easy to see: +# Bad +paste0( + "Requirement: ", requires, + "\n", "Result: ", + result, "\n") +``` + +## Braced expressions {#braced-expressions} + +Braced expressions, `{}`, define the most important hierarchy of R code, allowing you to group multiple R expressions together into a single expression. The most common places to use braced expressions are in function definitions, control flow, and in certain function calls (e.g. `tryCatch()` and `test_that()`). + +To make this hierarchy easy to see: -* `{` should be the last character on the line. Related code (e.g., an `if` - clause, a function declaration, a trailing comma, ...) must be on the same - line as the opening brace. +* `{` should be the last character on the line. + Related code (e.g., an `if` clause, a function declaration, a trailing comma, ...) must be on the same line as the opening brace. * The contents should be indented by two spaces. * `}` should be the first character on the line. -It is occassionally useful to have an empty curly braces block, in which case it should be written `{}`. - ```{r} # Good if (y < 0 && debug) { @@ -340,8 +401,6 @@ tryCatch( } ) -while (waiting_for_something()) {} - # Bad if (y < 0 && debug) { message("Y is negative") @@ -355,52 +414,120 @@ if (y == 0) message("x is negative or zero") } } else { y ^ x } +``` + +It is occasionally useful to have empty braced expressions, in which case it should be written `{}`, with no intervening space. + +```{r} +# Good +function(...) {} -while (waiting_for_something()) { } -while (waiting_for_something()) { +# Bad +function(...) { } +function(...) { } ``` +## Control flow + +### Loops + +R defines three types of looping constructs: `for`, `while`, and `repeat` loops. + +* The body of a loop must be a braced expression. + + ```{r} + # Good + for (i in seq) { + x[i] <- x[i] + 1 + } + + while (waiting_for_something()) { + cat("Still waiting...") + } + + # Bad + for (i in seq) x[i] <- x[i] + 1 + + while (waiting_for_something()) cat("Still waiting...") + ``` + +* It is occasionally useful to use a `while` loop with an empty braced expression body to wait. As mentioned in [Braced expressions](#braced-expressions), there should be no space within the `{}`. + ### If statements -* When present, `else` should be on the same line as `}`. +* A single line if statement must never contain braced expressions. You can use + single line if statements for very simple statements that don't have + side-effects and don't modify the control flow. -* `&` and `|` should never be used inside of an `if` clause because they can - return vectors. Always use `&&` and `||` instead. + ```{r} + # Good + message <- if (x > 10) "big" else "small" + + # Bad + message <- if (x > 10) { "big" } else { "small" } -* NB: `ifelse(x, a, b)` is not a drop-in replacement for `if (x) a else b`. - `ifelse()` is vectorised (i.e. if `length(x) > 1`, then `a` and `b` will be - recycled to match) and it is eager (i.e. both `a` and `b` will always be - evaluated). + if (x > 0) message <- "big" else message <- "small" - If you want to rewrite a simple but lengthy `if` block: + if (x > 0) return(x) + ``` + +* A multiline if statement must contain braced expressions. ```{r} + # Good + if (x > 10) { + x * 2 + } + if (x > 10) { - message <- "big" + x * 2 } else { - message <- "small" + x * 3 + } + + # Bad + if (x > 10) + x * 2 + + # In particular, this if statement will only parse when wrapped in a braced + # expression or call + { + if (x > 10) + x * 2 + else + x * 3 } ``` - Just write it all on one line: +* When present, `else` should be on the same line as `}`. + +* Avoid implicit type coercion (e.g. from numeric to logical) in the condition of an if statement: ```{r} - message <- if (x > 10) "big" else "small" + # Good + if (length(x) > 0) { + # do something + } + + # Bad + if (length(x)) { + # do something + } ``` -### Inline statements {#inline-statements} +::: {.callout-note} +`&` and `|` should never be used inside of an `if` clause because they can return vectors. Always use `&&` and `||` instead. +::: -It's ok to drop the curly braces for very simple statements that fit on one line, as long as they don't have side-effects. +::: {.callout-note} +`ifelse(x, a, b)` is not a drop-in replacement for `if (x) a else b`. `ifelse()` is vectorised (i.e. if `length(x) > 1`, then `a` and `b` will be recycled to match) and it is eager (i.e. both `a` and `b` will always be evaluated). +::: -```{r} -# Good -y <- 10 -x <- if (y < 20) "Too low" else "Too high" -``` +### Control flow modifiers {#control-flow-modifiers} -Function calls that affect control flow (like `return()`, `stop()` or `continue`) should always go in their own `{}` block: +Syntax that affects control flow (like `return()`, `stop()`, `break`, or `next`) should always go in their own `{}` block: ```{r} # Good @@ -415,31 +542,22 @@ find_abs <- function(x) { x * -1 } +for (x in xs) { + if (is_done(x)) { + break + } +} + # 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 } -``` - -### Implicit type coercion - -Avoid implicit type coercion (e.g. from numeric to logical) in `if` statements: - -```{r} -# Good -if (length(x) > 0) { - # do something -} -# Bad -if (length(x)) { - # do something +for (x in xs) { + if (is_done(x)) break } ``` @@ -472,77 +590,23 @@ switch(x, switch(y, 1, 2, 3) ``` -## Long function calls - -Strive to limit your code to 80 characters per line. This fits comfortably on a -printed page with a reasonably sized font. If you find yourself running out of -room, this is a good indication that you should encapsulate some of the work in -a separate function. +## Semicolons -If a function call is too long to fit on a single line, use one line each for -the function name, each argument, and the closing `)`. -This makes the code easier to read and to change later. +Semicolons are never recommended. +In particular, don't put `;` at the end of a line, and don't use `;` to put multiple commands on one line. ```{r} # Good -do_something_very_complicated( - something = "that", - requires = many, - arguments = "some of which may be long" -) +my_helper() +my_other_helper() # Bad -do_something_very_complicated("that", requires, many, arguments, - "some of which may be long" - ) -``` - -As described under [Named arguments](#argument-names), you can omit the argument names -for very common arguments (i.e. for arguments that are used in almost every -invocation of the function). If this introduces a large disparity between the line lengths, you may want to supply names anyway: +my_helper(); +my_other_helper(); -```{r} -# Good -my_function( - x, - long_argument_name, - extra_argument_a = 10, - extra_argument_b = c(1, 43, 390, 210209) -) - -# Also good -my_function( - x = x, - y = long_argument_name, - extra_argument_a = 10, - extra_argument_b = c(1, 43, 390, 210209) -) +{ my_helper(); my_other_helper() } ``` -You may place multiple unnamed arguments on the same line if they are closely -related to each other. A common example of this is creating strings -with `paste()`. In such cases, it's often beneficial to match one line of code -to one line of output. - -```{r} -# Good -paste0( - "Requirement: ", requires, "\n", - "Result: ", result, "\n" -) - -# Bad -paste0( - "Requirement: ", requires, - "\n", "Result: ", - result, "\n") -``` - -## Semicolons - -Don't put `;` at the end of a line, and don't use `;` to put multiple commands -on one line. - ## Assignment Use `<-`, not `=`, for assignment.