Author: Obinna (OBI) Obianom
Date: 2026-08-26

# Creating and using super variables with newSuperVar() in the quickcode R package

#### Obinna (OBI) Obianom

#### 2026-08-26

## What is a super variable

A regular R variable lives in whatever environment created it. Pass it into a function, and the function only sees a copy; change that copy inside the function, and the original outside is untouched. Assign inside a `for` loop or an `lapply()` call and, again, only the local copy changes. This is normal, sensible R scoping — but it also means that sharing genuinely mutable state across scopes (functions, loops, `apply`-family calls, nested environments) usually forces you to reach for `<<-`, explicit environments, or reference-class objects, all of which add ceremony to what is conceptually a simple idea: “I want one variable that stays the same object no matter where I touch it from.”

`newSuperVar()` creates exactly that: a super variable — a named object that is attached to the search path so it is visible and mutable from any scope in the current R session, with optional protections layered on top: type/class enforcement, an explicit lock, and a cap on how many times its value may be changed.

## Creating a super variable

    # Task: create a super variable to store a dataset that should not be altered
    newSuperVar(mtdf, value = austres) # create a super variable named mtdf
    head(mtdf) # view it like any other object

    mtdf.class # view the stored class of the variable, e.g. "ts"
    # once created, any new value assigned to mtdf via mtdf.set() MUST have this same class

`newSuperVar(variable, value = 0L, lock = FALSE, editn = NULL)` takes the (unquoted) name you want the super variable to have, the initial `value`, whether it should be `lock`ed, and `editn`, an optional cap on the number of times it may later be changed.

Two things happen the moment you call it:

1. The object `mtdf` becomes available by that name from anywhere in your R session — not just the environment where you called `newSuperVar()`.
2. A family of companion functions and an attribute, all prefixed with `mtdf.`, are attached alongside it (`mtdf.set()`, `mtdf.rm()`, `mtdf.contains()`, `mtdf.round()`, `mtdf.signif()`, `mtdf.class`, plus `mtdf.push()`/`mtdf.pop()`, and — for data frames specifically — `mtdf.head()`/`mtdf.tail()`).

Trying to declare a super variable under a name that already exists produces an error telling you to remove the existing one first (see `.rm()` below) rather than silently overwriting it.

## The attached methods

### `.set()` — updating the value

Ordinary reassignment (`mtdf <- something_else`) will not change the super variable itself; it just creates a local variable that happens to shadow the name in your current scope. To actually update the stored value, use `.set()`:

    newSuperVar(edtvec, value = number(5))
    edtvec # view content of the vector

    # edtvec.set(letters) # ERROR: Cannot set to a value with a different class
    #                      # than the original value (numeric here, letters is character)

    edtvec.set(number(20)) # set to new numbers
    edtvec # view updated output

Because `.set()` is a function, not a `<-` assignment, it works identically no matter which scope it is called from:

    for (pu in 1:8) {
      print(edtvec)          # view output within the loop
      edtvec.set(number(pu)) # set to new numbers within the for loop
    }

    lc <- lapply(1:8, function(pu) {
      print(edtvec)           # view output within the loop
      edtvec.set(number(pu))  # set to new numbers within the lapply loop
    })

    # compare that to an ordinary local variable, which lapply() cannot mutate:
    bim <- 198
    lc <- lapply(1:8, function(j) {
      print(bim)
      bim <- j # only changes the local copy inside this call; outer bim is untouched
    })

### Type safety is automatic

Every call to `.set()` checks the class of the new value against the class the super variable was created with, and refuses the update (with an informative error) if they don’t match. This is what makes super variables safer to share across a large script than a plain global variable: a typo that assigns the wrong kind of value is caught immediately instead of silently corrupting downstream logic.

### `.round()` and `.signif()` — numeric rounding in place

For numeric or data-frame-of-numeric super variables, `.round(digits)` and `.signif(digits)` update the stored value in place, rounding to the given number of decimal places or significant digits respectively:

    newSuperVar(mtdf3, value = beaver1, lock = TRUE)
    head(mtdf3) # view original values

    mtdf3.round(1) # round to 1 decimal place
    head(mtdf3) # view rounded values

    mtdf3.signif(2) # round to 2 significant digits
    head(mtdf3) # view again

### `.contains()` — searching the value

`.contains()` checks whether a pattern or value is present anywhere in the super variable, using the same matching options as `grep()` (`ignore.case`, `perl`, `fixed`, `useBytes`, `invert`). For vectors it searches the vector directly; for a data frame, matrix, or array it searches within a specific column, named via the `df.col` argument:

    # Task: create and search a data frame
    newSuperVar(lon2, value = mtcars) # declares lon2
    lon2 # view content of lon2

    lon2.contains("21.0", df.col = "mpg") # search the mpg column
                                            # for the character "21.0"
    lon2.contains(21.0, df.col = "mpg")    # search the mpg column
                                            # for the numeric value 21.0

    # remove lon2 as a super variable once done with it
    exists("lon2") # TRUE, before removal
    lon2.rm()
    exists("lon2") # FALSE, after removal

    # Task: create and search a vector
    newSuperVar(lon3, value = number(10, seed = 12)) # declares lon3
    lon3 # view content of lon3

    lon3.contains(72)               # TRUE/FALSE - does the vector contain 72?
    lon3.contains(72, fixed = TRUE) # same search, using fixed (non-regex) matching

    lon3.rm() # remove lon3 as a super variable

### `.push()` and `.pop()` — building a new value from the current one

`.push(add, .df = c("row", "col"))` and `.pop(n = 10, .df = c("row", "col"))` compute what the super variable’s vector, list, or data frame would look like with an element added or removed (for data frames, a row or column, controlled by `.df`), and return that result — they do not update the stored value on their own. To actually persist the change, feed the result back into `.set()`:

    newSuperVar(queue, value = c(1, 2, 3))

    queue.push(4)          # returns c(1, 2, 3, 4), queue itself is still c(1, 2, 3)
    queue.set(queue.push(4)) # now queue is actually updated to c(1, 2, 3, 4)

    queue.set(queue.pop(1))  # drop the last element and persist the change

### `.head()` and `.tail()` — previewing data frames

When a super variable’s initial value is a data frame, two extra helpers are attached automatically: `.head(n = 10)` and `.tail(n = 10)`, printing the first or last `n` rows. They are not created for non-data-frame super variables.

### `.rm()` — removing a super variable

`.rm()` detaches the super variable and all of its companion functions, freeing up the name for reuse (including re-declaring it with `newSuperVar()` again):

    exists("lon3") # TRUE
    lon3.rm()
    exists("lon3") # FALSE

## Locking a super variable

Passing `lock = TRUE` at creation time adds an extra layer of protection around the stored binding, intended for values you want to guard more strictly against being changed by anything other than the super variable’s own methods:

    newSuperVar(mtdf3, value = beaver1, lock = TRUE)

Locked or not, updates always go through `.set()`, `.round()`, or `.signif()` — those methods know how to handle the lock internally, so day-to-day usage looks identical whether `lock` is `TRUE` or `FALSE`. Use `lock = TRUE` for values you want to flag as intentionally protected, such as reference datasets or configuration you don’t want casually reassigned elsewhere in a long script.

## Limiting how many times a value can change

`editn` caps the number of successful `.set()` calls a super variable will accept:

    # Task: create a super variable that can only be edited 3 times
    newSuperVar(man1, value = number(5), editn = 3)
    man1 # view value

    man1.set(number(10)) # change value - 1st edit, succeeds
    man1

    man1.set(number(2))  # change value - 2nd edit, succeeds
    man1

    man1.set(number(1))  # change value - 3rd edit, succeeds
    man1

    man1.set(number(5))  # 4th attempted edit - does NOT change the value,
    man1                  # because the maximum of 3 edits has been used up

- `editn = NULL` (the default) allows unlimited edits.
- `editn = 0` prevents editing entirely — in this case, `.set()` is not even attached to the super variable, so there is no way to change its value short of removing it with `.rm()` and creating it again.
- Any positive integer allows exactly that many successful edits before further `.set()` calls silently stop taking effect.

## Use cases

Super variables are a good fit when you want:

- Protected reference data. A dataset, lookup table, or configuration object that should survive accidental reassignment deep inside a script, without you having to audit every place the name is used.
- State shared across scopes. A counter, accumulator, or running result that needs to be updated from inside loops, `lapply()`/`sapply()` calls, or nested functions, without threading it through as an explicit argument and return value everywhere.
- Guarded mutability. Values that should be changeable, but only through a single, auditable entry point (`.set()`), and optionally only a limited number of times (`editn`).
- Type-safe globals. A global-like variable that refuses updates of the wrong class, catching an entire category of bugs at the point of assignment rather than somewhere downstream.

## A note on naming

Because a super variable’s companion functions are derived from its name by appending suffixes like `.set`, `.rm`, and `.class`, avoid using names that already end in a period followed by one of those words for something else in the same session, and avoid creating two super variables whose derived helper names would collide. If you need a fresh super variable under a name you’ve already used, call `<name>.rm()` first — `newSuperVar()` deliberately refuses to redeclare an existing super variable in place.

## See also

- `vignette("quickcode_r_introduction")` for a broader tour of the package
- `vignette("not_functions_data_validation_r")` for the `not.*` family used throughout quickcode’s own internals, including inside `newSuperVar()`