Better packages

My R package development creds

I really ❤️ R package development

My R package development creds

Contributed to

What is good code

The only way to write good code is to write tons of shitty code first. Feeling shame about bad code stops you from getting to good code

Hadley Wickham’s 2015 tweet

What is good code?

If you’re an experienced programmer and you’re tempted to code-shame someone, try thinking back to your own million lines of bad code. Imagine someone had mocked you then (…). Would you have continued asking for help? Would you have ever gotten through your million lines?

David Robinson’s blog post “A Million Lines of Bad Code”

Today’s workshop

I’ll present a collection of very useful things I’ve learnt over the past few years.

After each section I’ll summarize and ask you to comment.

Then you pick up one thing to improve in your package, and open a PR.

Then another short slidedeck.

Then you get to review someone else’s PR!

Interface

Nice messages

Get to know the cli package

variable <- 42
cli::cli_alert_info("Set {.field parameter} to {.val {variable}}")#>  Set parameter to 42

Vignette to migrate from usethis::ui functions to cli

Nice messages

How to control verbosity? How to shup your package up?

  • argument in each function 😩

  • global option à la usethis.quiet

cli_alert_info <- function(...) {
  if (!getOption("usethis.quiet", default = FALSE)) {
    cli::cli_alert_info(...)
  }
}

Nice messages

Further reading: https://github.com/ropensci/dev_guide/issues/603

🧰 Are there messages in your package you could improve?

Error messages

cli::cli_abort(
  c(
    "Can't find good error message.",
    i = "Read the tidyverse style guide."
  )
)#> Error:
#> ! Can't find good error message.
#>  Read the tidyverse style guide.

Error messages

🧰 Go through your package’s error messages (look for stop() and equivalents). Could some of them be improved by applying the tidyverse guidance?

Argument checks

Further reading: Checking the inputs of your R functions by Hugo Gruson , Sam Abbott , Carl Pearson.

Arguments checks

🧰 Does your package document and validate arguments? Improve this in one function.

Interface 🎤 stop() 🎤

  • Nice messages with {cli}.
  • Error messages with {cli}, tidyverse style guide.
  • Argument checks with rlang, R-hub blog post.

Please post in the chat

  • Something you found interesting!
  • Something you disagreed with!
  • A recent good/bad experience with these tools?

Less code or less headaches

Weigh your dependencies

Does this dependency spark joy? 😉

  • A dependency is code that someone else carefully crafted and tested!
  • A dependency is a failure point.

Further reading: Dependencies: Mindset and Background in the R Packages book by Hadley Wickham and Jenny Bryan.

Weigh your dependencies

In rOpenSci dev guide

  • curl, httr2, httr. Not RCurl.

  • jsonlite. Not rjson nor RJSONIO.

  • xml2. Not XML

  • sf, spatial suites developed by the r-spatial and rspatial communities. Not sp, rgdal, maptools, rgeos.

Weigh your dependencies

🧰 Are there dependencies you could add, replace or remove in your package?

Less code? Beyond using dependencies

Feature creep: “excessive ongoing expansion or addition of new features in a product” https://en.wikipedia.org/wiki/Feature_creep

Okay to split the package.

Okay to say no to feature requests. Example

Less code

🧰 Are there feature requests you’d like to say no to? Save answer as GitHub reply?

Less code 🎤 stop() 🎤

  • Choosing dependencies.
  • Dependencies to avoid.
  • Defining package scope.

Please post in the chat

  • Something you found interesting!
  • Something you disagreed with!
  • A recent good/bad experience with these tools?

Code

Code as simple as possible: early returns

👀

do_this <- function() {
  if (!is_that_present()) {
    return(NULL)
  } else {
    # more code
    return(blip)
  }
}

Code as simple as possible: early returns

do_this <- function() {
  if (!is_that_present()) {
    return(NULL)
  } 
  # more code
  
  blip
}

Code as simple as possible: early returns

Further reading: Code Smells and Feels by Jenny Bryan

🧰 Look at logic in one or a few functions. Could you simplify it with early returns, helper functions?

Code aesthetics

Some of it only relevant if you see code.

  • Use alignment?
  • Use paragraphs
  • Use “header” comments for navigation.

Code alignment

  • Align argument in function definitions.

  • More vertical alignment? I am not sensitive to it. 😇

Paragraphs

One paragraph = one idea (works for writing prose too!).

Vertical space is costly (what fits on the screen?)

head <- collect_metadata(website)
head_string <- stringify(head)

body <- create_content(website)
body_string <- stringify(body)

Header comments

At least in RStudio IDE, outline on the right. In any case good to indicate high-level structure within a script.

# Header level 1 ----
more code

## Header level 2 ----
more code

Code aesthetics

🧰 Open one or a few scripts, can you improve the aesthetics?

Less comments / self explaining code

Comments are like little alerts. Don’t create fatigue!

Comments that repeat the code get out of date.

Less comments / self explaining code

# use only non empty strings
if (!is.na(x) && nzchar(x)) {
  use_string(x)
}

Less comments / self explaining code

is_non_empty_string <- function(x) {
  !is.na(x) && nzchar(x)
}

if (is_non_empty_string(x)) {
  use_string(x)
}

Less comments / self-explaining code

Further reading: https://blog.r-hub.io/2023/01/26/code-comments-self-explaining-code/

🧰 Are there opportunities for less comments (or more comments!) in some of your scripts?

Code 🎤 stop() 🎤

  • Early returns.
  • Code aesthetics.
  • Less comments/self-explaining code.

Please post in the chat

  • Something you found interesting!
  • Something you disagreed with!
  • A recent good/bad experience with these tools?

Test code

DAMP / DRY

DAMP: descriptive and meaningful.

DRY: don’t repeat yourself.

A trade-off!

Test code vs code

Code is covered by test code so we can take more risks!

Ideal tests

Example: {swamp}

Let’s explore https://github.com/maelle/swamp

🧰 Do some of your tests have top-level code? Can you create helper files and helper functions, and repeat object creation in each test?

Escape hatches

My code



is_internet_down <- function() {
  !curl::has_internet()
}

my_complicated_code <- function() {
  if (is_internet_down()) {
    message("No internet! Le sigh")
  }
  # blablablabla
}

How to test for the message?

Escape hatch

Let’s add a switch/escape hatch

is_internet_down <- function() {

  if (nzchar(Sys.getenv("TESTPKG.NOINTERNET"))) {
    return(TRUE)
  }

  !curl::has_internet()
}

Escape hatches

In the test,



test_that("my_complicated_code() notes the absence of internet", {
  withr::local_envvar("TESTPKG.NOINTERNET" = "blop")
  expect_message(my_complicated_code(), "No internet")
})

Escape hatches

Further reading: https://blog.r-hub.io/2023/01/23/code-switch-escape-hatch-test/

Escape hatches

🧰 do you have such a situation to test?

Test code 🎤 stop() 🎤

  • DAMP & DRY
  • Test code vs code
  • Ideal tests (self contained, can be run interactively, no leak)
  • Escape hatches

Please post in the chat

  • Something you found interesting!
  • Something you disagreed with!
  • A recent good/bad experience with these tools?

Choose your own adventure

…with your own package! In breakout rooms.

⚠️ Make Pull Requests and don’t merge them yet! ⚠️

We’ll gather in XX minutes as a group to discuss.