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
AddRow()
Permute()

# Bad
RowAdder()
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
    LongFunctionName <- 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
    LongFunctionName <- 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
LongFunctionName <- 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 return()

Do not rely on R’s implicit return feature. It is better to be clear about your intent to return() an object (ref).

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

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

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

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

# Bad
FindAbs <- function(x) {
  if (x > 0) return(x)
  return(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:

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

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