A composable column expression — the reified upgrade over the closure-based
surfaces (the original row-predicate closure) and pre-materialised Series
columns. An Expr is built through the constructor functions
(col / lit / lit_* / when), the operator impls, and the methods,
so every tree is well-formed by construction; the when chain stages
through the WhenThen / WhenThenElse types below, completing as a
Ternary. Building one is
total — unknown columns and dtype mismatches surface at evaluation time
(in frame), never here.
The type is opaque: it wraps an ExprNode AST and exposes no
variants. Outside this package an expression is a value you build and pass
on, or render with to_string — there is no matching on its shape. The AST
itself is module-internal, in a package a downstream module cannot import
at all, so no caller can name a node, match
one, or hold one: adding a node for a new operator breaks nobody.
The shape is not observable either: there is no == on an expression. One
existed, comparing the two trees, and it made how an operator lowers a
promise to callers — normalising a tree or merging two node kinds would
have changed what compared equal without changing what any expression
means. to_string() is what a caller inspects with instead; it renders
what it prints, so two literal series differing only in their cells render
alike.
The engine reads the shape through four accessors beside the type —
node() hands over the wrapped AST, and children() /
referenced_columns() / output_name() answer its introspection
questions. frame and lazy are in-module and match ExprNode
directly.
The wrapped AST carries ExprNode children, not Expr, so this package
bridges the two: a constructor unwraps its child Exprs with .node, and
the children accessor below wraps ExprNode children back into Expr. The
nodes, in construction-route order:
- Col / Lit / LitSeries — leaves (col, lit / lit_*,
lit_series); a LitSeries embeds a pre-materialised Series;
- Binary — arithmetic + - * /, comparisons, Kleene & / |;
- Unary — -e, .not(), the null / NaN / finiteness probes, the dt_*
calendar quartet, .abs() / .floor() / .ceil() / .sign() /
.round(), and the always-Float math family .sin() … .sqrt()
(.atan2() / .hypot() ride Binary);
- Agg — .sum() / .mean() / … reductions;
- Str — the .str_* namespace;
- Positional — the history-dependent transforms (.shift(n) /
.diff(n), the .cum_* running aggregates, .rank(how~),
.arg_sort(), the .rolling_* windowed statistics,
.forward_fill() / .backward_fill(), .unique_counts(),
.is_first_distinct() / .is_last_distinct()), whose cell reads its
neighbours or the column's occurrence structure;
- Cast — .cast(dtype); Alias — .with_alias(name);
- Ternary — when(c).then(a).otherwise(b);
- FillNull / FillNan — .fill_null(v) / .fill_nan(v), dedicated nodes
(not lowered to a Ternary) so the operand appears once and a chained
coalesce stays linear;
- Diff / Clip — .diff(n) / .clip(lower?, upper?), likewise dedicated
(not lowered to - shift(n) / a nested when) so the operand appears
once and a chained difference or clamp stays linear;
- IsIn / IsBetween — the membership / range predicates, likewise
dedicated so the operand is evaluated once;
- Map / MapBatches — the row-wise / batched closure escape hatches
(.map_elements() / map_many() / .map_batches()), the function opaque
so each is identified by its (label, inputs) (MapBatches also by its
returns_scalar flag).