design icon indicating copy to clipboard operation
design copied to clipboard

Make it clear what you are branching over

Open hadley opened this issue 6 years ago • 8 comments

is_foofy <- function(x) x %in% c("a", "b", "c")

# How do you make clear which cases need to be handled?
if (is_foofy(x)) {
  if (x == "a") {
    
  } else if (x == "b") {
    
  } else if (x == "c") {
    
  }
}

# How do you make the 3 cases clear?
n_unnamed <- sum(!named)
if (n_unnamed == 0) {
  # do nothing
} else if (n_unnamed == 1) {
  clauses <- c(clauses, build_sql("ELSE ", input[!named]))
} else {
  stop("Can only have one unnamed (ELSE) input", call. = FALSE)
}

hadley avatar Jan 10 '19 16:01 hadley

Principle: explicitly enumerate all options?

hadley avatar Jan 10 '19 16:01 hadley

  if (is.call(fun)) {
    if (is_namespaced_dplyr_call(fun)) {
      call[[1]] <- fun[[3]]
    } else if (is_tidy_pronoun(fun)) {
      stop("Use local() or remote() to force evaluation of functions", call. = FALSE)
    } else {
      return(eval_bare(call, env))
    }
  }

Unappealing because one clause continues and two exit

hadley avatar Jan 11 '19 14:01 hadley

My approaches to these problems, for discussion:

is_foofy() seems redundant if we have is_foofy_a() etc.?

clauses <- c(clauses, else_clause(input[!named])

...

else_clause <- function(input) {
  if (is_empty(input)) return()
  if (!has_length(input, 1)) stop(...)
  ...
}
  if (is.call(fun) && !is_namespaced_dplyr_call(fun)) {
    if (is_tidy_pronoun(fun)) {
      stop("Use local() or remote() to force evaluation of functions", call. = FALSE)
    }
    return(eval_bare(call, env))
  }

  call[[1]] <- fun[[3]]

krlmlr avatar Apr 23 '19 16:04 krlmlr

Ensure each branch is mutually exclusive?

hadley avatar May 01 '19 01:05 hadley

The else or the shortcut via stop() or return() makes them mutually exclusive by definition. Do we need an else if there is also a shortcut?

I'm always taking a second look if a function has multiple if() clauses that are not connected with an else. Is this issue related to cyclomatic complexity of individual functions? Can we suggest that each function uses at most one control flow structure?

krlmlr avatar May 01 '19 08:05 krlmlr

Is this related with the notion that "surprises" should be in branches, and the default control flow should describe the "regular" path?

# Good

if (surprise()) {
  abort("oops")
}

carry_on()


# Bad

if (expected()) {
  return(some_result())
}

abort("oops")

krlmlr avatar May 06 '19 07:05 krlmlr

I don't think one style is good or bad, but mixing the two is bad. I regularly conclude functions with an exception. If you have a series of surprises, they should all exit non-locally, and If you have a series of expectations, they should all exit locally? With the obvious exception of the type checking at the beginning of the function with non-local exits, which should be taken as its own paragraph inside the function.

lionel- avatar May 07 '19 06:05 lionel-

I'm working on a good example where this feels rather desirable (and unavoidable):

if (expected()) {
  return(some_result())
}

abort("oops")

It's a function to process HTTP responses. The case of status 200, JSON Content-Type is easy to handle and you confirm and exit ASAP for that. Then we go on to throw various sorts of errors for all the other weird stuff that happens. In any case, it's a good type of task to bear in mind here.

jennybc avatar May 07 '19 16:05 jennybc