Sign in

    ihb2032/MoonFrame/expr does not have a README file

    Expr

    pub struct Expr {
    // private fields
    }

    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 probes, and .abs() / .floor() / .ceil() / .sign() / .round();
    • Agg — .sum() / .mean() / … reductions;
    • Str — the .str_* namespace;
    • 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;
    • 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).
    impl Add for Expr
    impl BitAnd for Expr
    impl BitOr for Expr
    impl Div for Expr
    impl Mul for Expr
    impl Neg for Expr
    impl Show for Expr
    impl Sub for Expr
    impl Debug for Expr

    Expr::abs

    fn Expr::abs(self : Expr) -> Expr

    a.abs() — absolute value. Int → Int (Int64::MIN wraps, like Neg); Float → Float (|NaN| = NaN, |-0.0| = 0.0). A null stays null; a non-numeric operand raises TypeMismatch.

    Expr::add

    fn Expr::add(self : Expr, other : Expr) -> Expr

    Expr::cast

    Cast the operand to target at evaluation time, delegating to Series::cast (so the supported dtype pairs — and the Unsupported cases — are exactly the eager ones).

    Expr::ceil

    fn Expr::ceil(self : Expr) -> Expr

    a.ceil() — round toward +∞ to an integer value. Float → Float; Int →Int unchanged. ±inf / NaN pass through. A null stays null; a non-numeric operand raises TypeMismatch.

    Expr::count

    fn Expr::count(self : Expr) -> Expr

    Count of non-null cells of the operand over the evaluation scope (Series::count semantics).

    Expr::div

    fn Expr::div(self : Expr, other : Expr) -> Expr

    Expr::eq

    fn Expr::eq(self : Expr, other : Expr) -> Expr

    a.eq(b) — elementwise equality, producing a Bool column. The whole comparison family is methods, not operators: MoonBit pins the Eq / Compare traits to Bool / Int returns, so they cannot build an Expr — and the postfix method binds tighter than & / | anyway, so a.gt(x) & b.lt(y) needs none of the parentheses Polars' operator comparisons force. A null on either side nulls the output cell.

    Expr::fill_nan

    fn Expr::fill_nan(self : Expr, value : Expr) -> Expr

    Replace every NaN cell of self with value, keeping the non-NaN cells (including true nulls) verbatim — Polars' fill_nan. The dual of fill_null: where fill_null replaces missing cells and leaves NaN (a value) alone, fill_nan replaces NaN and leaves nulls alone.

    It evaluates exactly like when(self.is_not_nan()).then(self).otherwise(value) — named after self (the operand), and a null cell — for which is_not_nan is null — falls through the Kleene ternary to a null result, never to value. The branches unify their dtype like any ternary (Int meets Float by promoting to Float, any other mismatch is a TypeMismatch). Unlike that ternary spelling, the dedicated FillNan node holds (and evaluates) self once, so a chain of fills stays linear in tree size and work. Building the node is total.

    Expr::fill_null

    fn Expr::fill_null(self : Expr, value : Expr) -> Expr

    Replace every null cell of self with value, keeping the non-null cells verbatim — Polars' fill_null (the value form), and a coalesce of self over value when value is itself a column. value is any expression: a literal (lit_int(0)), another column (col("fallback")), or a computed tree.

    It evaluates exactly like when(self.is_not_null()).then(self).otherwise(value) — the result is named after self (the filled column), never after value, so with_columns([col("x").fill_null(...)]) replaces "x" in place. The branches must unify their dtype the way a ternary's do — Int meets Float by promoting to Float, any other mismatch is a TypeMismatch at evaluation. A non-null NaN is a value (validity 1), so it is kept, not filled; only true nulls are replaced. Unlike that ternary spelling, the dedicated FillNull node holds (and evaluates) self once, so the chained-coalesce idiom (col("a").fill_null(col("b")).fill_null(lit_int(0))) stays linear in tree size and work — the lowering embedded self twice and grew exponentially with chain length. Building the node is total; errors surface when a consuming verb evaluates it — and the value branch is evaluated even where nothing needs filling, so fill_null(lit(Scalar::Null)) is an Unsupported from the null literal, not a no-op.

    Expr::first

    fn Expr::first(self : Expr) -> Expr

    First cell of the operand over the evaluation scope, in row order, keeping the operand's dtype. Positional — a null first cell is null, an empty scope is null, and a present NaN passes through verbatim.

    Expr::floor

    fn Expr::floor(self : Expr) -> Expr

    a.floor() — round toward −∞ to an integer value. Float → Float; Int →Int unchanged (an integer is its own floor). ±inf / NaN pass through. A null stays null; a non-numeric operand raises TypeMismatch.

    Expr::floor_div

    fn Expr::floor_div(self : Expr, other : Expr) -> Expr

    a.floor_div(b) — floor (integer) division, rounding the quotient toward negative infinity (Polars //). Named, not an operator: MoonBit's // is a line comment. Same-dtype Int / Int → Int (-7 // 2 = -4, not the -3 truncation gives); any Float operand promotes the result to Float (floor(a / b)). Int division by zero yields a null cell (integers have no infinity, and a backend integer divide would trap); Float division by zero follows IEEE 754 (±inf / nan) like /. A null on either side nulls the output cell; a non-numeric operand raises TypeMismatch.

    Expr::ge

    fn Expr::ge(self : Expr, other : Expr) -> Expr

    a.ge(b) — elementwise >= (Bool column, null-propagating).

    Expr::gt

    fn Expr::gt(self : Expr, other : Expr) -> Expr

    a.gt(b) — elementwise > (Bool column, null-propagating).

    Expr::is_between

    fn Expr::is_between(self : Expr, lo : Expr, hi : Expr, closed? :
    ClosedInterval
    ) -> Expr

    a.is_between(lo, hi) — a Bool column, true where a falls in the range. closed picks which endpoints count (Polars' closed): Both (the default) is lo <= a <= hi, Left / Right open the other end, and None excludes both. Equivalent to the matching ge / gt and le / lt pair joined by land — it inherits their exact Int/Float ordering, String / Bool ordering, Kleene null propagation, and TypeMismatch on an unorderable pair — but as a dedicated node the operand a is evaluated once.

    Expr::is_in

    fn Expr::is_in(self : Expr, members : Array[
    Scalar
    ]) -> Expr

    a.is_in(members) — a Bool column, true where the cell equals one of the literal members. Each member is compared exactly as a.eq(lit(member)) would — the general path is an OR of eq over the set, and a String / Bool / Int column whose members share its dtype takes an equivalent single-pass membership test instead: Int / Float members compare across types exactly (no 2^53 collision), and a member whose dtype cannot compare with the column raises TypeMismatch. A Null member matches nothing (it has no value to equal), an empty set is false for every present cell, and a null cell yields null.

    Expr::is_nan

    fn Expr::is_nan(self : Expr) -> Expr

    Bool column that is true where the operand holds the IEEE NaN value (Polars' is_nan). The operand must be numeric: an Int cell is never NaN (false), a Float cell is tested by Double::is_nan, and a non-numeric operand is a TypeMismatch at evaluation. Unlike is_null, this propagates nulls — a missing cell is neither NaN nor not, so the result cell is null — since NaN is a real value distinct from a missing one. Building the node is total.

    Expr::is_not_nan

    fn Expr::is_not_nan(self : Expr) -> Expr

    Bool column that is true where the operand is a non-NaN numeric value — the complement of is_nan on non-null cells, propagating nulls the same way (a missing cell stays null). Same numeric-operand requirement.

    Expr::is_not_null

    fn Expr::is_not_null(self : Expr) -> Expr

    Bool column that is true where the operand is non-null — the complement of is_null, equally total.

    Expr::is_null

    fn Expr::is_null(self : Expr) -> Expr

    Bool column that is true where the operand is null. Reads validity only, so the result is itself never null (total).

    Expr::land

    fn Expr::land(self : Expr, other : Expr) -> Expr

    Expr::last

    fn Expr::last(self : Expr) -> Expr

    Last cell of the operand over the evaluation scope, in row order, keeping the operand's dtype. The positional mirror of first.

    Expr::le

    fn Expr::le(self : Expr, other : Expr) -> Expr

    a.le(b) — elementwise <= (Bool column, null-propagating).

    Expr::lor

    fn Expr::lor(self : Expr, other : Expr) -> Expr

    Expr::lt

    fn Expr::lt(self : Expr, other : Expr) -> Expr

    a.lt(b) — elementwise < (Bool column, null-propagating).

    Expr::map_batches

    fn Expr::map_batches(self : Expr, label~ : String, returns_scalar? : Bool, f : (
    Series
    ) ->
    Series
    raise
    DataError
    ) -> Expr

    Apply a host closure to self's whole evaluated column at once — the batched escape hatch (Polars' map_batches). Where map_elements hands f one Scalar per row, map_batches hands it the entire Series and takes back a Series, so a vectorised kernel (a cumulative sum, a rank, a rolling window) runs once over the column rather than cell by cell. label names the step in explain / Show; the closure is opaque to introspection, to rendering, and to the optimizer, which treats the node as a barrier because it cannot see through it.

    Building the node is total; f runs at evaluation and may raise. The returned series rides the same length contract as lit_series: a frame-tall result passes through, a length-1 result broadcasts, any other length raises LengthMismatch in the consuming verb. The result is named after self, so with_alias renames it, and its backend is canonicalised (an all-valid numeric result lands on Numeric) so collect ≡ execute.

    Set returns_scalar when f reduces its input to a length-1 series: it marks the node as a per-group reduction so it is accepted as a custom aggregation inside group_by(...).agg([...]), where f receives each group's rows and must return a length-1 series (a non-length-1 result raises LengthMismatch at collect). Left false (the default), the node is a plain row-wise expression and the optimizer treats it — like map_elements — as a value-and-shape barrier that no filter sinks across.

    Expr::map_elements

    Apply a host closure to each row of self — the single-input escape hatch (Polars' map_elements). Where the operators and methods above cover the documented algebra, map_elements reaches past it: f receives the operand's cell as a Scalar (a null cell as Scalar::Null) and returns the output cell. label names the step in explain / Show output — the function itself is opaque to introspection, to rendering, and to the optimizer, which treats the node as a barrier because it cannot see through the closure.

    Building the node is total; f runs at evaluation, once per row, and may raise (the error propagates from the consuming verb). The output column's dtype is that of the first non-null Scalar the closure returns, except that a closure returning both Int and Float cells promotes the column to Float — the engine's Int → Float rule — rather than nulling whichever type came second. An all-null (or empty) result has no such cell, so its dtype falls back to self's own dtype, yielding an all-null (or empty) column of that dtype rather than the Unsupported a bare Null literal raises (Polars' tolerance of a null-returning map). Only the dtype is borrowed; the result's backend follows its own content, as every computed column's does. The result is named after self (its leftmost column), so with_alias renames it. Use it inside with_columns / select / filter like any other expression; the optimizer treats it as a value barrier (it can raise on values), so no filter sinks across it.

    Expr::max

    fn Expr::max(self : Expr) -> Expr

    Maximum of the operand over the evaluation scope (Series::max semantics: NaN and nulls are skipped).

    Expr::mean

    fn Expr::mean(self : Expr) -> Expr

    Mean of the operand over the evaluation scope (Series::mean semantics: NaN propagates, nulls are skipped).

    Expr::median

    fn Expr::median(self : Expr) -> Expr

    Median of the operand over the evaluation scope. Nulls are skipped, and a NaN propagates (like sum / mean; min / max are the order statistics that skip it, and sort orders NaN as the largest value). An all-missing scope reduces to null, and a non-numeric operand is TypeMismatch. Always a Float (Int widens), so an even count averages its two middles.

    Expr::min

    fn Expr::min(self : Expr) -> Expr

    Minimum of the operand over the evaluation scope (Series::min semantics: NaN and nulls are skipped).

    Expr::modulo

    fn Expr::modulo(self : Expr, other : Expr) -> Expr

    a.modulo(b) — remainder (Polars %; a named method, % maps to no Expr operator). Same-dtype Int / Int → Int carrying the dividend's sign (-7 % 2 = -1); any Float operand promotes to Float (IEEE remainder). Int modulo by zero yields a null cell (as for floor_div); Float modulo by zero is NaN. A null on either side nulls the output cell; a non-numeric operand raises TypeMismatch.

    Expr::mul

    fn Expr::mul(self : Expr, other : Expr) -> Expr

    Expr::n_unique

    fn Expr::n_unique(self : Expr) -> Expr

    Number of distinct non-null values of the operand over the evaluation scope (Series::n_unique semantics: every NaN is one bucket, -0.0 folds into +0.0). Total over every dtype, never null — an Int.

    Expr::ne

    fn Expr::ne(self : Expr, other : Expr) -> Expr

    a.ne(b) — elementwise inequality (Bool column, null-propagating).

    Expr::neg

    fn Expr::neg(self : Expr) -> Expr

    Expr::not

    fn Expr::not(self : Expr) -> Expr

    Kleene logical negation of a Bool expression (not(null) = null). A method because MoonBit has no overloadable unary ~ / !.

    Expr::output

    fn Expr::output(self : Expr, logger : &Logger) -> Unit

    Expr::pow

    fn Expr::pow(self : Expr, other : Expr) -> Expr

    a.pow(b) — exponentiation, always Float (Int operands promote to Double, like /): total for every base / exponent — a negative exponent, a negative or fractional base, and overflow all resolve under IEEE 754 (0.0 ** 0.0 = 1.0, an out-of-range result is ±inf, an invalid one NaN). A null on either side nulls the output cell; a non-numeric operand raises TypeMismatch.

    Expr::round

    fn Expr::round(self : Expr, decimals? : Int) -> Expr

    a.round() — round to the nearest integer value, ties to even (banker's rounding: 2.5 and 3.5 both round to 2 and 4, 0.5 → 0), matching Polars' default. Float → Float; Int → Int unchanged (an integer is its own rounding). ±inf / NaN pass through and ±0.0 keeps its sign. A null stays null; a non-numeric operand raises TypeMismatch. decimals (default 0) rounds to that many decimal places — decimals=2 sends 1.005 to 1.0 or 1.01 as binary floating point dictates, the same caveat Polars carries. A negative decimals clamps to 0, and an Int column is the identity at any setting. So is a place finer than the value's own resolution: round(decimals=20) returns 123456.789 unchanged rather than perturbing it by an ulp. (The other rounding modes remain a deferred additive refinement; this is the ties-to-even form.)

    Expr::sign

    fn Expr::sign(self : Expr) -> Expr

    a.sign() — -1 / 0 / +1 by sign, in the operand's own dtype (Int →Int, Float → Float). Float NaN stays NaN and ±0.0 gives 0.0. A null stays null; a non-numeric operand raises TypeMismatch.

    Expr::std

    fn Expr::std(self : Expr) -> Expr

    Sample standard deviation of the operand over the evaluation scope (ddof = 1, Polars' default): NaN propagates through the mean, nulls are skipped, fewer than two non-null cells reduce to null, and a non-numeric operand is TypeMismatch. Always a Float.

    Expr::str_contains

    fn Expr::str_contains(self : Expr, pattern : String, literal? : Bool) -> Expr

    A Bool column that is true where the cell contains pattern — Polars' str.contains. literal defaults to true, matching pattern as a plain substring; literal=false reads it as a POSIX regular expression (the core engine's dialect, so character classes are [[:digit:]] / [[:alpha:]], not the PCRE \d / \w, which raise). The default is the opposite of Polars', which is regex-first. A regex is compiled once per evaluation, so an invalid pattern raises InvalidOperation then. Null cells stay null.

    Expr::str_count_matches

    fn Expr::str_count_matches(self : Expr, pattern : String) -> Expr

    Count the non-overlapping matches of the POSIX regular expression pattern in each cell — an Int column (Polars' str.count_matches), 0 where the pattern does not match. An all-valid result rides the Numeric fast path, like str_len_chars. The regex dialect and the invalid-pattern error are str_contains's; a null cell stays null and a non-String operand raises TypeMismatch.

    Expr::str_ends_with

    fn Expr::str_ends_with(self : Expr, suffix : String) -> Expr

    A Bool column that is true where the cell ends with suffix (Polars str.ends_with). Null cells stay null.

    Expr::str_extract

    fn Expr::str_extract(self : Expr, pattern : String, group? : Int) -> Expr

    Extract a substring matched by the POSIX regular expression pattern — a nullable String column (Polars' str.extract). group (default 0, the whole match — Polars defaults to the first capture group 1 instead) selects a capture group; a cell that does not match, or whose chosen group did not participate, yields null. The regex dialect and the invalid-pattern error are str_contains's; a null cell stays null and a non-String operand raises TypeMismatch.

    Expr::str_len_bytes

    fn Expr::str_len_bytes(self : Expr) -> Expr

    The number of UTF-8 bytes in each cell of a String column, as an Int (Polars str.len_bytes) — the encoded byte length, so an ASCII character is 1, a é is 2, and a supplementary-plane emoji is 4 (vs str_len_chars, which counts every character as 1). Null cells stay null; an all-valid result rides the Numeric fast path.

    Expr::str_len_chars

    fn Expr::str_len_chars(self : Expr) -> Expr

    The number of Unicode characters in each cell of a String column, as an Int (Polars str.len_chars): a supplementary-plane character counts once, not as its two UTF-16 code units. Null cells stay null; an all-valid result rides the Numeric fast path like every computed numeric column.

    Expr::str_pad_end

    fn Expr::str_pad_end(self : Expr, width : Int, fill? : Char) -> Expr

    Right-pad each cell of a String column with fill until it is width characters long — Polars' str.pad_end, the mirror of str_pad_start.

    Expr::str_pad_start

    fn Expr::str_pad_start(self : Expr, width : Int, fill? : Char) -> Expr

    Left-pad each cell of a String column with fill until it is width characters long — Polars' str.pad_start. A cell already at least width characters is unchanged (never truncated); null cells stay null. The width counts characters, consistent with str_len_chars.

    Expr::str_replace

    fn Expr::str_replace(self : Expr, pattern : String, value : String, literal? : Bool) -> Expr

    Replace the first occurrence of pattern with value in each cell of a String column — Polars' str.replace. literal defaults to true (plain substring); literal=false reads pattern as a POSIX regular expression (same dialect and invalid-pattern error as str_contains), with value inserted literally — no capture-group references yet. A cell without a match is unchanged; null cells stay null.

    Expr::str_replace_all

    fn Expr::str_replace_all(self : Expr, pattern : String, value : String, literal? : Bool) -> Expr

    Replace every occurrence of pattern with value in each cell (Polars str.replace_all), the all-occurrences mirror of str_replace — including its literal parameter and default.

    Expr::str_reverse

    fn Expr::str_reverse(self : Expr) -> Expr

    Reverse the Unicode characters of each cell of a String column — Polars' str.reverse(). Surrogate pairs are respected (reversal is by codepoint, not UTF-16 unit); null cells stay null.

    Expr::str_slice

    fn Expr::str_slice(self : Expr, offset : Int, length? : Int) -> Expr

    Substring of each cell by character position — Polars' str.slice. offset is a 0-based character index; a negative offset counts from the end (-2 starts two characters before the end). length is the number of characters (omitted → to the end); a length of zero or less yields the empty string. Both are clamped to the cell — an offset past the end gives "", and a length past the end stops at the end — so it never raises on a value. Character-based (surrogate pairs are never split), consistent with str_len_chars; a null cell stays null and a non-String operand raises TypeMismatch.

    Expr::str_split_get

    fn Expr::str_split_get(self : Expr, sep : String, index : Int) -> Expr

    The index-th field of each cell split on the literal separator sep, as a nullable String (a scalar slice of Polars' str.split, which returns a list this repo has no dtype for). index is 0-based; a cell with fewer than index + 1 fields (or a negative index) yields null. "a,b,c" split on "," at index 1 is "b". Null cells stay null. An empty separator splits into single characters (index 0 of "abc" is "a") — a deliberate spelling of the underlying split, where Polars' would offer a leading empty field.

    Expr::str_starts_with

    fn Expr::str_starts_with(self : Expr, prefix : String) -> Expr

    A Bool column that is true where the cell starts with prefix (Polars str.starts_with). Null cells stay null.

    Expr::str_strip_chars

    fn Expr::str_strip_chars(self : Expr, chars? : String) -> Expr

    Strip leading and trailing characters from every cell of a String column — Polars' str.strip_chars. With chars omitted, the default set is the Unicode White_Space property (ASCII whitespace plus NBSP, the narrow no-break space, the ideographic space, …); with chars given, every character in that string is a strip target (order and repeats do not matter). Null cells stay null.

    Expr::str_to_lowercase

    fn Expr::str_to_lowercase(self : Expr) -> Expr

    Lowercase every cased letter of a String column — Polars' str.to_lowercase, the case mirror of str_to_uppercase: default conversion, expansions and all (İ lowercases to i + combining dot above).

    Expr::str_to_uppercase

    fn Expr::str_to_uppercase(self : Expr) -> Expr

    Uppercase every cased letter of a String column — Polars' str.to_uppercase — through the Unicode default case conversion: the simple one-to-one mappings (é → É, Greek and Cyrillic across their alphabets) plus the SpecialCasing expansions, so ß uppercases to SS and the fi ligature to FI — a cell's length can grow. The locale-conditional rules are deliberately absent: this is the default conversion, which carries no locale (the Turkish dotless-i rules stay a caller's rewrite). Characters with no case mapping (CJK, emoji, digits, …) pass through. Null cells stay null, the result keeps the operand's column name, and a non-String operand is a TypeMismatch at evaluation. The leading method of the string namespace.

    Expr::str_zfill

    fn Expr::str_zfill(self : Expr, width : Int) -> Expr

    Left-pad each cell of a String column with '0' until it is width characters long — Polars' str.zfill. Like str_pad_start('0') but sign-aware: a leading '+' / '-' keeps its place and the zeros are inserted after it ("-5" to width 4 is "-005", not "00-5"). A cell already at least width characters is unchanged (never truncated); null cells stay null. The width counts characters, consistent with str_len_chars.

    Expr::sub

    fn Expr::sub(self : Expr, other : Expr) -> Expr

    Expr::sum

    fn Expr::sum(self : Expr) -> Expr

    Sum of the operand over the evaluation scope (whole frame, or one group under agg). Inherits Series::sum semantics: NaN propagates, nulls are skipped, non-numeric operands are TypeMismatch.

    Expr::to_repr

    Expr::to_string

    fn Expr::to_string(self : Expr) -> String

    Expr::variance

    fn Expr::variance(self : Expr) -> Expr

    Sample variance of the operand over the evaluation scope (ddof = 1); the square of std, with the identical null / NaN / dtype rules. Always a Float. Spelled variance because var is a reserved MoonBit word (the with_alias / .land() situation), so Polars' var becomes variance.

    Expr::with_alias

    fn Expr::with_alias(self : Expr, name : String) -> Expr

    Name the result column ((col("revenue") - col("cost")).with_alias("profit")). Without an alias, an expression is named after its leftmost column reference, or "literal" for a column-less tree. Called with_alias — alias itself is a MoonBit reserved word.

    WhenThen

    pub struct WhenThen {
    // private fields
    }

    First step of the conditional chain when(cond).then(a).otherwise(b) — holds the Bool condition until then supplies the matching branch. Constructible only through when, which is a pub function and so cannot return a priv type — that, and nothing else, is why the struct is pub. Its field is private, so the only thing a caller can do with a WhenThen is call then.

    WhenThen::then

    fn WhenThen::then(self : WhenThen, value : Expr) -> WhenThenElse

    Supply the branch taken where the condition is true.

    WhenThenElse

    pub struct WhenThenElse {
    // private fields
    }

    Second step of the conditional chain — condition plus then branch, waiting for otherwise to complete the expression. Same private-field rationale as WhenThen: the only move is otherwise.

    WhenThenElse::otherwise

    fn WhenThenElse::otherwise(self : WhenThenElse, value : Expr) -> Expr

    Supply the branch taken where the condition is false, completing the conditional as a Ternary expression node.

    col

    fn col(name : String) -> Expr

    Reference a column by name, for the Polars-style call shape col("a") + col("b"). The name resolves at evaluation time — ColumnNotFound then if the frame has no such column; building the reference itself is total. A free function, like the lit* family — the facade re-exports it.

    Example

    test "col: renders as a reference" {
    inspect(col("qty").to_string(), content="col(qty)")
    }

    cols

    fn cols(names : Array[String]) -> Array[Expr]

    Build a column-reference list from names: cols(["a", "b"]) is exactly [col("a"), col("b")]. The ergonomic shorthand for the common case of projecting (or dropping) several existing columns by name through the expression verbs — df.select(cols(["a", "b"])) reads almost like the names-only projection it replaces. Each name resolves at evaluation time, exactly like col; building the list is total.

    Example

    test "cols: shorthand for the reference list" {
    debug_inspect(
    cols(["a", "b"]).map(e => e.to_string()),
    content="[\"col(a)\", \"col(b)\"]",
    )
    }

    lit

    Embed a literal Scalar. At evaluation it becomes a length-1 column broadcast against its siblings. Prefer the typed shorthands (lit_int / lit_float / lit_str / lit_bool), which avoid spelling the Scalar variant at call sites.

    Example

    test "lit: embeds any Scalar" {
    inspect(lit(@types.Scalar::Int(3L)).to_string(), content="3")
    }

    lit_bool

    fn lit_bool(value : Bool) -> Expr

    Bool literal.

    Example

    test "lit_bool: renders bare" {
    inspect(lit_bool(true).to_string(), content="true")
    }

    lit_float

    fn lit_float(value : Double) -> Expr

    Float literal.

    Example

    test "lit_float: whole values keep their point" {
    inspect(lit_float(2.0).to_string(), content="2.0")
    }

    lit_int

    fn lit_int(value : Int64) -> Expr

    Int literal (64-bit, like every MoonFrame integer cell).

    Example

    test "lit_int: renders bare" {
    inspect(lit_int(5L).to_string(), content="5")
    }

    lit_series

    fn lit_series(series :
    Series
    ) -> Expr

    Embed a pre-materialised Series as a literal column, so a ready-made column joins a pipeline beside the declarative col(...) ... expressions (df.with_columns([lit_series(s), (col("a") + col("b")).with_alias("c")])). At evaluation the series is used as-is: a length-1 series broadcasts over the evaluation height (like a scalar lit), a series whose length matches the frame supplies one cell per row, and any other length raises LengthMismatch. The result column keeps the series' own name unless with_alias overrides it — so with_columns([lit_series(s)]) adds, or in-place replaces, a column named s.name(). A free function (the argument is a Series, not an Expr), like the lit_* family and map_many. Building the node is total; the length check happens at evaluation, in frame.

    Example

    test "lit_series: names its column after the series" {
    let s = @series.Series::from_ints("score", [1, 2])
    inspect(lit_series(s).output_name(), content="score")
    }

    lit_str

    fn lit_str(value : String) -> Expr

    String literal.

    Example

    test "lit_str: renders quoted" {
    inspect(lit_str("ab").to_string(), content="\"ab\"")
    }

    map_many

    Apply a host closure across several input columns, row by row — the multi-input escape hatch, and the reified replacement for the original closure filter predicate: f is handed one Scalar per input in inputs order (null cells as Scalar::Null) and is called for every row, so a row predicate is a map_many(..., f) returning a Bool. A free function rather than a method (there is no single self); inputs may mix columns, literals, and aggregations, the length-1 results broadcasting over the row count. Same opacity, totality, dtype inference, naming (after the leftmost input), and value-barrier rules as map_elements — with the dtype fallback reading the leftmost input, whatever it is, so only an empty inputs leaves an all-null result with no witness and raises Unsupported. The inputs array is copied, so mutating it after construction cannot alter the built expression.

    Example

    test "map_many: drops to a host closure" {
    let total = map_many(label="total", [col("a"), col("b")], cells => {
    @types.Scalar::Int(cells[0].as_int() + cells[1].as_int())
    })
    inspect(total.to_string(), content="map(\"total\", [col(a), col(b)])")
    }

    when

    fn when(cond : Expr) -> WhenThen

    Open a conditional expression: when(cond).then(a).otherwise(b) evaluates to a where cond is true, b where it is false, and null where cond is null. The chain is the only construction route, so a conditional is complete by the time it becomes an Expr.

    Example

    test "when: the chain is the only construction route" {
    let tagged = when(col("qty").gt(lit_int(5)))
    .then(lit_str("big"))
    .otherwise(lit_str("small"))
    inspect(
    tagged.to_string(),
    content="when((col(qty) > 5)).then(\"big\").otherwise(\"small\")",
    )
    }