T provides powerful metaprogramming capabilities inspired by Lisp and
R’s rlang package. These features allow you to capture code
as data, manipulate it, and evaluate it dynamically.
rlang::quo).expr vs
quoT has two families of quotation functions, matching R’s
rlang:
| Function | Result | Environment | Use when… |
|---|---|---|---|
to_expr(x) |
Expression |
None | You only need the AST |
quo(x) |
Quosure |
Captured at call site | You need the AST + its lexical context |
to_exprs(...) |
List[Expression] |
None | Multiple naked expressions |
quos(...) |
List[Quosure] |
Captured at call site | Multiple expressions with lexical context |
to_expr(expression)The to_expr() function captures the code as a naked
Expression object. The current environment is
not stored.
e = to_expr(1 + 2)
print(e)
-- Output: to_expr(1 + 2)
quo(expression)The quo() function captures the code as a
Quosure — a pair of the expression and its lexical
environment. When later evaluated with eval(), the
expression runs in the captured environment, not the caller’s.
x = 10
q = quo(1 + x) -- captures x = 10 in the environment
x = 99
eval(q) -- returns 11, not 100
to_exprs(...)to_exprs() captures multiple expressions and returns
them as a list of naked Expression objects. It supports named
arguments.
ee = to_exprs(x = 1 + 1, y = 2 + 2)
-- Result: [x: to_expr(1 + 1), y: to_expr(2 + 2)]
quos(...)quos() captures multiple expressions as a list of
Quosures, each paired with the current lexical environment.
x = 10
qs = quos(a = 1 + x, b = 2 * x)
-- Result: [a: quo(1 + x), b: quo(2 * x)]
-- Both quosures capture x = 10 in their environment.
In T, if you use a word that isn’t defined as a variable, it is automatically treated as a Symbol when inside a quoting context. This is useful for building Domain Specific Languages (DSLs).
e = to_expr(select(df, age, height))
-- 'select', 'age', and 'height' are captured as symbols.
eval(expr_or_quosure)The eval() function evaluates an Expression or Quosure:
- Expression: evaluated in the current
environment. - Quosure: evaluated in its
captured environment.
e = to_expr(10 + 20)
eval(e) -- evaluates in current env → 30
x = 5
q = quo(x + 1) -- captures x = 5
x = 100
eval(q) -- evaluates in captured env (x = 5) → 6
get(name)The get() function provides a way to retrieve a
variable’s value dynamically by its name (as a String or Symbol). This
matches R’s get() semantics and is useful when you have a
variable name stored in a string.
salary = [1000, 2000, 3000]
var_name = "salary"
get(var_name) -- retrieves [1000, 2000, 3000]
get(to_symbol(var_name)) -- also works with Symbols
Important: get() resolves names in the calling
environment, not in a data-masking context. To retrieve a
column dynamically inside a data verb, use quasiquotation with
!!to_symbol(col).
Quasiquotation allows you to “fill in the blanks” in a captured expression.
!! (Unquote)The !! (pronounced “bang-bang”) operator evaluates its
operand immediately and injects the result into the surrounding quoted
expression. When the operand is a Quosure, only the expression part is
injected (the environment is stripped).
x = 10
e = to_expr(1 + !!x)
print(e)
-- Output: to_expr(1 + 10)
inner = quo(1.5 + 2.5)
outer = to_expr(2 * !!inner) -- !! strips env from quosure
print(outer)
-- Output: to_expr(2 * (1.5 + 2.5))
!!! (Unquote-Splice)The !!! (pronounced “triple-bang”) operator evaluates
its operand and splices the elements into the
surrounding call or list. The operand must evaluate to a
List, Vector, or Dict. Quosures
in the spliced list have their environments stripped.
vals = [1, 2, 3]
e = to_expr(sum(!!!vals))
print(e)
-- Output: to_expr(sum(1, 2, 3))
If you splice a named List, the names are used as argument names in the resulting call.
my_args = [x: 10, y: 20]
e = to_expr(f(!!!my_args, z: 30))
print(e)
-- Output: to_expr(f(x = 10, y = 20, z = 30))
!!name := value (Dynamic
Naming)The !!name := value syntax allows you to use a
dynamically computed string or symbol as the name of an argument or list
element inside a quoting context. The left-hand side (name)
must evaluate to a String or Symbol.
col = "age"
e = to_expr(mutate(df, !!col := 42))
print(e)
-- Output: to_expr(mutate(df, age = 42))
If !!name does not evaluate to a String or
Symbol, a TypeError is raised.
to_symbol(string_or_symbol)When you have a column or argument name in a string variable, use
to_symbol() to turn it into a runtime Symbol
that can be injected with !!.
col_name = "mpg"
to_expr(select(df, !!to_symbol(col_name)))
-- Output: to_expr(select(df, mpg))
This is most useful for programmatic code generation. Use
to_symbol() to turn a string into a label that
!! can inject as a symbol, or that get() can
use for variable lookup.
For writing functions that accept unevaluated expressions from the
caller — similar to dplyr verbs in R — T provides
enquo() and enquos().
enquo(param)enquo() must be called inside a function body. It
captures the expression AND the caller’s environment
for the named argument param, returning a Quosure. This is
the quosure equivalent of enquo() in R’s rlang.
my_select = \(df: DataFrame, col: Any -> DataFrame) {
col_expr = enquo(col) -- captures expr + caller's env
eval(to_expr(df |> select(!!col_expr)))
}
my_select(iris, $Sepal.Length)
-- Equivalent to: iris |> select($Sepal.Length)
enquo() accepts exactly one argument, which must be a
bare symbol (the name of one of the function’s parameters).
\(df, $col)T now supports auto-quoting directly in lambda and
function(...) parameter lists. Prefixing a parameter with
$ means the caller can pass a bare column name and the
function receives it as a symbol-like column reference.
my_mean = \(df, $col) {
summarize(df, result = mean(!!col))
}
df = to_dataframe([salary: [100, 200, 300]])
my_mean(df, salary)
This removes the need for the common enquo() +
eval(to_expr(...)) wrapper when the function is simply
forwarding a column-like argument into an NSE-aware data verb.
Current behavior:
salary$salarySymbol in
ordinary code!!col inside NSE-aware verbs expands back to a column
reference so summarize(result = mean(!!col)) works as
expectedUse enquo() when you need to preserve the caller’s full
expression rather than just a column-style name.
enquos(...)enquos() is the variadic counterpart to
enquo(). It captures all expressions passed through the
variadic ... parameter as a named list of Quosures, each
paired with the caller’s environment.
my_summarize = \(df: DataFrame, ... -> DataFrame) {
cols = enquos(...) -- list of quosures from the caller
eval(to_expr(df |> summarize(!!!cols)))
}
my_summarize(iris,
mean_sepal = mean($`Sepal.Length`),
mean_petal = mean($`Petal.Length`))
-- Evaluates to: iris |> summarize(mean_sepal = ..., mean_petal = ...)
enquos() is called with ... or with no
arguments; both capture the variadic expressions from the enclosing
call.
You can use quasiquotation to dynamically build pipeline nodes or intents:
var_name = "mpg"
my_intent = to_expr(intent {
target = !!var_name
method = "lm"
})
print(my_intent)
-- Output: to_expr(intent { target = "mpg"; method = "lm" })
T also supports a Lisp-style prefix call syntax which integrates seamlessly with quotation:
e = to_expr((add, 1, 2))
-- Equivalent to to_expr(add(1, 2))
| Operator/Function | Purpose |
|---|---|
to_expr(x) |
Capture x as a naked
Expression (no environment). |
to_exprs(...) |
Capture multiple expressions as a List of naked Expressions. |
quo(x) |
Capture x as a Quosure
(expression + lexical environment). |
quos(...) |
Capture multiple expressions as a List of Quosures. |
eval(e) |
Evaluate Expression e in the
current env, or Quosure e in its captured env. |
get(name) |
Dynamically retrieve a variable’s value by String or Symbol name. |
to_symbol(s) |
Convert a String s into a
Symbol at runtime. |
!!x |
Evaluate x and inject into
to_expr()/quo(); strips env from
quosures. |
!!!x |
Evaluate x and splice
elements into to_expr()/quo(). |
!!name := value |
Use a dynamic String/Symbol as an argument
name inside to_expr()/quo(). |
enquo(param) |
Inside a function: capture caller’s
expression for param as a Quosure. |
enquos(...) |
Inside a function: capture all variadic expressions as a List of Quosures. |
When an Expression or Quosure is evaluated inside a data verb (like
mutate, filter, or summarize), it
uses a Data Mask.
T handles column resolution using a specific prefixing logic to avoid collisions with global functions:
$: Expressions like
$score are parsed as ColumnRef.$score.score) and the prefixed name
($score) for every column in the current DataFrame.score is defined in the global
environment (e.g., as a function), it does not
interfere with $score.-- Global 'score' function
score = \(x, y) x + y
df = [score: 1, 2, 3]
-- This works correctly because '$score' looks for '$score' in the mask,
-- ignoring the global 'score' function.
df |> mutate(new = $score * 2)
quo by default: When in doubt, use
quo instead of expr. It ensures the code
“remembers” its environment, preventing NameError when
evaluated in different contexts.$param for
the common “accept a column name” case. Use enquo when you
need the caller’s full expression, and use to_symbol() when
you are starting from a computed string.!!name := !!value
for maximum flexibility when writing generic data processing
functions.