3  Functions

3.1 Naming

As well as following the general advice for object names in Section 2.1, strive to use verbs for function names:

# Good
add_row()
permute()

# Bad
row_adder()
permutation()

3.2 Anonymous functions

Use the new lambda syntax: \(x) x + 1 when writing short anonymous functions (i.e. when you define a function in an argument without giving it an explicit name).

# Good
map(xs, \(x) mean((x + 5)^2))
map(xs, function(x) mean((x + 5)^2))

# Bad
map(xs, ~ mean((.x + 5)^2))

Don’t use \() for multi-line functions:

# Good
map(xs, function(x) {
  mean((x + 5)^2)
})

# Bad
map(xs, \(x) {
  mean((x + 5)^2)
})

Or when creating named functions:

# Good
cv <- function(x) {
  sd(x) / mean(x)
}

# Bad
cv <- \(x) sd(x) / mean(x)

Avoid using \() in a pipe, and remember to use informative argument names.

3.3 Multi-line function definitions

There are two options if the function name and definition can’t fit on a single line. In both cases, each argument goes on its own line; the difference is how deep you indent it and where you put ) and {:

  • Single-indent: indent the argument name with a single indent (i.e. two spaces). The trailing ) and leading { go on a new line.

    # Good
    long_function_name <- function(
      a = "a long argument",
      b = "another argument",
      c = "another long argument"
    ) {
      # As usual code is indented by two spaces.
    }
  • Hanging-indent: indent the argument name to match the opening ( of function. The trailing ) and leading { go on the same line as the last argument.

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

These styles are designed to clearly separate the function definition from its body.

# 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, and to see all three function arguments
}

If a function argument can’t fit on a single line, this is a sign you should rework the argument to keep it short and sweet.

3.4 S7

In S7, the method definition can be long because the function name is replaced by a method call that specifies the generic and dispatch classes. In this case we recommend the single-indent style.

method(from_provider, list(openai_provider, class_any)) <- function(
  provider,
  x,
  ...,
  error_call = caller_env()
) {
  ...
}

If the method definition is too long to fit on one line, use the usual rules to spread the method arguments across multiple lines:

method(
  from_provider,
  list(openai_provider, class_any, a_very_long_class_name)
) <- function(
  provider,
  x,
  ...,
  error_call = caller_env()
) {
  ...
}

3.5 return()

Only use return() for early returns. Otherwise, rely on R to return the result of the last evaluated expression.

# 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)
}

Return statements should always be on their own line because they have important effects on the control flow. See also inline statements.

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

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

If your function is called primarily for its side-effects (like printing, plotting, or saving to disk), it should return the first argument invisibly. This makes it possible to use the function as part of a pipe. print methods should usually do this, like this example from httr:

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

3.6 Comments

In code, use comments to explain the “why” not the “what” or “how”. Each line of a comment should begin with the comment symbol and a single space: #.

# 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)

Comments should be in sentence case, and only end with a full stop if they contain at least two sentences:

# 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)