---
title: "Checks with rlang-style errors"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Checks with rlang-style errors}
  %\VignetteEncoding{UTF-8}
  %\VignetteEngine{knitr::rmarkdown}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

```{r setup}
library(zmisc)
```

The `chk_*()` functions check the type and shape of an argument and, on failure,
raise an [rlang]-style error naming the argument as the caller wrote it. Each
function returns its input, so a check can sit in the middle of a pipe, and each
is cheap enough on the passing path to leave at the top of any function. The
checking is backed by the [checkmate] package.

```{r basics}
# The check returns its input, so it composes in a pipe
c(2, 4, 6) |> chk_numeric(length = 3) |> sum()

# On failure, the error names the argument as the caller wrote it
my_mean <- function(x) {
  chk_numeric(x)
  sum(x) / length(x)
}
tryCatch(my_mean("seven"), error = wrap_error)
```

## Scalars and atomic vectors

Every atomic type is covered in both a scalar and a vector form.

| R type       | Scalar            | Vector              |
| ------------ | ----------------- | ------------------- |
| any type     | `chk_scalar()`    | `chk_atomic()`      |
| `logical`    | `chk_flag()`      | `chk_logical()`     |
| `character`  | `chk_string()`    | `chk_character()`   |
| `numeric`    | `chk_number()`    | `chk_numeric()`     |
| `integer`    | `chk_inumber()`   | `chk_integer()`     |
| `double`     | `chk_dnumber()`   | `chk_double()`      |
| integerish   | `chk_znumber()`   | `chk_integerish()`  |
| naturalish   | `chk_count()`     | `chk_naturalish()`  |
| `factor`     |                   | `chk_factor()`      |
| `complex`    |                   | `chk_complex()`     |
| `raw`        |                   | `chk_raw()`         |
| `Date`       | `chk_day()`       | `chk_date()`        |
| `POSIXct`    | `chk_instant()`   | `chk_posixct()`     |

*integerish* means a functional integer: a number very close to a whole number,
whether stored as `integer` or as `double`. *naturalish* restricts that to the
natural numbers, zero and up.

Every one of them takes the same set of arguments: `na.ok`, `null.ok`,
`attr.ok`, and `length` or `range` where they apply, plus `zero.ok` for the
naturalish pair. The dots are reserved, so a misspelled argument raises rather
than being quietly ignored.

```{r params}
tryCatch(chk_character(c("a", NA), na.ok = FALSE), error = wrap_error)
tryCatch(chk_integer(1:5, length = 3), error = wrap_error)
tryCatch(chk_numeric(c(1, 99), range = c(0, 10)), error = wrap_error)
tryCatch(chk_count(0, zero.ok = FALSE), error = wrap_error)
```

`length` and `range` are pairs. A scalar pins both ends, `NA` at an end means
no bound there, and the same rule applies whether the pair counts elements,
counts characters, or bounds values.

```{r pairs}
chk_character(letters, length = c(10, NA)) |> length()
chk_string("abc", range = c(1, 3)) |> nchar()
```

`attr.ok` lists the attributes `x` may carry beyond those intrinsic to its type,
defaults to `"names"`, and takes `FALSE` for none at all or `TRUE` for any.

```{r attrs}
labelled_ages <- structure(c(38L, 41L), label = "Age at interview")
tryCatch(chk_integer(labelled_ages), error = wrap_error)
chk_integer(labelled_ages, attr.ok = "label") |> sum()
```

## Lists and composite objects

Container checks take `null.ok`. `chk_list()` additionally takes `length` with
the same semantics as for atomic vectors. `chk_environment()` additionally takes
`contains` as a list of item names that must be present in the environment.

```{r composite}
chk_data_frame(mtcars) |> nrow()
chk_list(list(a = 1, b = 2), length = 2) |> names()

# A data.frame is a list to typeof(), but not to chk_list()
tryCatch(chk_list(mtcars), error = wrap_error)
```

`chk_environment()`, `chk_data_table()` and `chk_tibble()` complete the set.

## Classes and conditions

`chk_class()` checks inheritance, and `chk_true()` is the catch-all: any
property of any object that can be written as a condition, at the cost of a
message that can only report that the condition was not met.

```{r other}
tryCatch(chk_class(1:3, "factor"), error = wrap_error)
tryCatch(chk_true(nrow(mtcars) > 100), error = wrap_error)
```

`chk_that()` is parallel to `chk_true()`, but with the value and the expression
separated, so that the condition can be applied to an object passing through a
pipe. The value is bound to `.` by default.

```{r chk_that}
mtcars |> chk_that(nrow(.) > 10) |> ncol()
tryCatch(mtcars |> chk_that(nrow(.) > 100), error = wrap_error)
```

`chk_dots_empty()` fails if anything was passed through `...`, and `chk_match()`
matches an argument against the values in its own default, returning the match.
`chk_match()` can be used *either* instead of `match.arg()` for a choice style
argument (in which case `x` must be a symbol), or as an way to check that an
arbitrary `character` value is an element of a set.

```{r match}
plot_kind <- function(kind = c("scatter", "line", "bar")) {
  kind <- chk_match(kind)
  kind
}
plot_kind()
tryCatch(plot_kind("pie"), error = wrap_error)
```

## Alternatives

Each `chk_*()` function states one thing. A requirement that is satisfied by
either of two shapes is written with `chk_any()`, which evaluates its arguments
in turn, returns the value of the first that passes, and otherwise raises one
error reporting every failure.

```{r chk_any}
x <- "a"
chk_any(chk_string(x), chk_number(x)) |> toupper()

y <- TRUE
tryCatch(chk_any(chk_string(y), chk_number(y)), error = wrap_error)
```

`chk_any()` relies on other `chk_*()` functions being aware that they are being
called by `chk_any()` and does not catch arbitrary errors. An error is never
raised unless all the checks fail, so a composite `chk_any()` call is not
unbearably slow. `chk_any()` on two passing elementary checks takes 10-20
microseconds compared to 1-4 microseconds for the elementary checks themselves.
If the first elementary check fails, this goes up to around 50 microseconds,
compared with around a millisecond (1000 microseconds) for a minimally caught
actual error.

Only the direct `chk_any()` calls itself are handled. Calls reached through a
helper function, or from inside a lambda passed to `lapply()` will error
directly, and so does everything that is not a failed check: a misspelled
function, an argument that does not exist, an object that was never bound.

The arguments are captured as expressions and evaluated in the calling
environment, so `...` cannot be forwarded into `chk_any()` from another
function, and an object cannot be piped into it. Both raise an error.

## Reference

The full argument documentation is in the help files, one per group:
[chk_atomic], [chk_composite] and [chk_other].

[chk_atomic]:    https://torfason.github.io/zmisc/reference/chk_atomic.html
[chk_composite]: https://torfason.github.io/zmisc/reference/chk_composite.html
[chk_other]:     https://torfason.github.io/zmisc/reference/chk_other.html

[checkmate]:     https://mllg.github.io/checkmate/
[rlang]:         https://rlang.r-lib.org/
