When two roads diverge in a yellow wood, you often have to take one as a traveler. In software development, we often face such difficult decisions as developers. Sometimes one road may seem to be “obviously” better than the other, so you make the decision for your potential users, and only support that road.
The only problem is “obviously”. Obviousness is in the eye of the beholder.
When designing the knitr package, I thought it was obvious that the working directory when running a code chunk should be the parent directory of the input file, instead of whatever the current getwd() is. This decision turned out to be the one that users hated most. I think I can still defend my decision, but I can also see their point and cannot deny the confusion caused by this decision. Anyway, I didn’t block the other way.
To Docker or not to Docker
Recently, Patrick, one of the contributors of xaringan, wrote a function decktape() to export slides to PDF via DeckTape, which looked like this originally:
decktape = function(file, output) {
args = shQuote(c(file, output))
system2('docker', c(
'run', '--rm', '-t', '-v', '`pwd`:/slides', '-v',
'$HOME:$HOME', 'astefanutti/decktape', args
))
}
He liked Docker, and I could definitely see the benefits of Docker. DeckTape can run with or without Docker, like any other software packages, but DeckTape is a NodeJS package, so you know… The lovely dependency hell there…
When I saw Patrick’s function, I decided to make it work without Docker, too. The reason was that the alternative way was trivially easy to implement. For conciseness, the four dots .... below indicates identical code in the above function so that you can easily see the extra code I added:
decktape = function(...., docker = Sys.which('decktape') == '') {
....
if (docker) system2(....) else system2('decktape', args)
}
That is low cost in implementation, but high return in usability. There might be users who’d rather deal with the Node dependency hell instead of pulling a Docker image of several hundred megabytes.1 It is not necessary to block their way or convince them why Docker is the ultimate best solution.
BTW, note that the default docker = Sys.which('decktape') == '' means if the user has chosen to install decktape locally and it can be found via the PATH variable, xaringan::decktape() will not use Docker (i.e., docker = FALSE). The default should reflect the user’s intention reasonably well. If the guess is incorrect, they can manually specify docker = TRUE or FALSE.
From File or not from File
Here is another example of two roads in a wood: jeroen/jsonlite#265. I don’t mean to criticize the bestest ninja in the R community (i.e., Jeroen), but this example is very representative in my eyes. I want to talk about it so others can learn from it. Basically, when there is a function that is supposed to deal with text data, it is common that the first argument of the function can be either the text or a file path (or a URL, etc.). A common trick is like this:
process_text = function(x) {
if (file.exists(x)) x = readLines(x)
paste(x, collapse = '\n')
}
So both process_text('foo/bar.txt') and process_text(c('a', 'b')) will work. This looks very neat, but the problem is, what if the input x is literally a character string that only happens to be a real file path?2 And what if the user entered a wrong file path by accident?3 The cleverness will bite us in these cases. What I often do is this:
process_text = function(x, content = readLines(x)) {
paste(content, collapse = '\n')
}
It provides both ways to users. They can process either a text file, or a character string. They know more about their data, and we don’t have to guess for them.
process_text('foo/bar.txt')
# if foo/bar.txt is not meant to be a file path
process_text(content = 'foo/bar.txt')
# signal an error if the file path is wrong
process_text('foo/baz.txt')
Actually jsonlite::fromJSON() is cleverer than that in guessing. It has multiple rules to decide whether the input is text or a file path, but no matter how clever it is, it still can backfire if the user doesn’t have a way to tell it if the input is text or a file path, because guessing, is guessing, after all.
# what if there is really a file named [1]?
xfun::in_dir(tempdir(), {
f = '[1]'
writeLines('[1, 2, 3]', f)
on.exit(file.remove(f))
jsonlite::fromJSON(f) # expecting c(1, 2, 3) but returns c(1)
})
FWIW, Jeroen has added a new function jsonlite::parse_json() that only deals with text input, which is great!
In short, we could absolutely love one road, or be clever on one road. Just make sure you have thought about whether it makes sense to block the other road.

-
Well, if you
npm install -g decktape, you may also end up with using a lot of your disk space because of the waynpmmanages packages. ↩︎ -
The function should return the character string, instead of reading the file. ↩︎
-
The function should signal an error instead of returning the character string of the wrong file path. ↩︎
Donate
As a freelancer (currently working as a contractor) and a dad of three kids, I truly appreciate your donation to support my writing and open-source software development! Your contribution helps me cope with financial uncertainty better, so I can spend more time on producing high-quality content and software. You can make a donation through methods below.
-
Venmo:
@yihui_xie, or Zelle:xie@yihui.name -
Paypal
-
If you have a Paypal account, you can follow the link https://paypal.me/YihuiXie or find me on Paypal via my email
xie@yihui.name. Please choose the payment type as “Family and Friends” (instead of “Goods and Services”) to avoid extra fees. -
If you don’t have Paypal, you may donate through this link via your debit or credit card. Paypal will charge a fee on my side.
-
-
Other ways:
WeChat Pay (微信支付:谢益辉) Alipay (支付宝:谢益辉) 

When sending money, please be sure to add a note “gift” or “donation” if possible, so it won’t be treated as my taxable income but a genuine gift. Needless to say, donation is completely voluntary and I appreciate any amount you can give.
Please feel free to email me if you prefer a different way to give. Thank you very much!
I’ll give back a significant portion of the donations to the open-source community and charities. For the record, I received about $30,000 in total (before tax) in 2024-25, and gave back about $15,000 (after tax).