github ↗
docs menu

Module Talon.Expr

Expressions: typed computations over the columns of a frame.

An expression ('a, 's) t computes values that read as 'a from the columns of a frame, the ordered rows a verb or an enclosing expression gives it. Its shape 's is row, one value per row of the frame, or agg, one value per frame. Handles (Col) are row, reductions take row to agg, and literals and elementwise operations keep their operands' shape, so sum (w *. x) /. sum w is agg. 's is a covariant phantom, so let cutoff = Expr.float 15. generalizes, and no call site writes a shape.

Expressions are data. Building one reads nothing; a verb binds it to its input schema when the verb is applied, checks it and infers its type, reporting every problem at once. Write them inside Expr.( … ), where the operators below shadow OCaml's.

Types. Whatever a value meets fixes its type:

  • A handle of kind k binds a column whose type k reads (see Kind.provably_equal).
  • Operands meet at the one of their types that contains the others (Type.common). Types that do not meet are a problem: cast first.
  • A literal, or an expression of literals such as int 2 * int 50, takes the type of the operand it meets, which must hold each literal and the value of each integer operation of literals (Type.holds). Where it meets none, it takes its kind's default type: int64, float64, bool, string, date, datetime[ns, UTC] or duration[ns].
  • null, a const and a ($) result take the type of the operand they meet. Where they meet none, store gives them one. Without it, a const or $ result stands only as an argument of ($) or as what Query.values decodes, so if_ c (const a) (const b) is a problem, and a null stands nowhere.
  • Result types follow from operand types alone: each function below states its result's type.

Frames. A context supplies a frame, and frames nest:

  • select, derive and filter: the input's rows;
  • aggregate ~by: one group's rows, in input order;
  • over ~by ~order e: the enclosing frame, partitioned by by, each partition in order; results return to their rows;
  • window r: at each row, the rows of the enclosing frame around it (Windows);
  • per_list ls r: at each row, the elements of its lists ls, in list order (Lists).

A frame's order is its input order, and no expression reorders values without returning them to their rows.

Nulls. Elementwise operations are null where an operand is null, except where stated. Comparisons are Kleene: a comparison with null is null. Reductions skip nulls except rows, count and n_unique.

Expressions and outputs

#type row

The shape of expressions with one value per row of their frame.

#type agg

The shape of expressions with one value per frame.

#type ('a, +'s) t

The type for expressions of shape 's whose values read as 'a.

#type +'s out

The type for outputs of shape 's: named expressions, as select, derive and aggregate take them.

Outputs

#val (:=) : string -> ('a, 's) t -> 's out

name := e outputs e as the column name. It binds more loosely than every operator, so "late" := delay > float 15. needs no parentheses. e needs a column type: a const or ($) result needs store, and an option result is never one.

Raises Invalid_argument if name is not valid UTF-8.

#val keep : Sel.t -> row out

keep sel outputs the columns that sel selects, unchanged and under their names. It keeps a column of any type, extensions included.

#val across : 'a Kind.t -> Sel.t -> (string -> ('a, row) t -> 's out) -> 's out

across k sel f is the outputs f n (Col.v k n) for each name n that sel selects, in order: across Kind.float Sel.(prefix "wk") (fun n x -> n := over (rank x)). A selected column that k does not bind is a problem: narrow sel with Sel.of_kind. f runs when the verb is applied and must be pure.

#type 's column = {
  1. #column : 'a. string -> ('a, row) t -> 's out;
}

The type for functions of one column of any type.

#val each : Sel.t -> 's column -> 's out

each sel { column } is the outputs column n x for each name n that sel selects, in order, where x reads the column n at its own type, extensions included: each Sel.all { column = (fun n x -> n := rows - count x) }. column is polymorphic, so it applies only operations that take every type; each is checked against the column's type when the verb is applied: sum of a string column is a problem. An extension column is read without its declaration, so operations that order its values (min, (<), rank, …) or compute with them (sum, mean, …) are problems, and those that count, move, select or compare them for equality apply. column runs when the verb is applied and must be pure.

Literals

#val int : int -> (int, 's) t

int n is the integer n.

#val float : float -> (float, 's) t

float x is the float x. It may round to the type it takes.

#val bool : bool -> (bool, 's) t

bool b is the boolean b.

#val string : string -> (string, 's) t

string s is the text s.

Raises Invalid_argument if s is not valid UTF-8.

#val instant : Time.instant -> (Time.instant, 's) t

instant t is the instant t.

#val wall : Time.wall -> (Time.wall, 's) t

wall t is the wall clock t.

#val span : Time.span -> (Time.span, 's) t

span d is the span d.

#val date : Time.date -> (Time.date, 's) t

date d is the date d.

#val null : ('a, 's) t

null is null, typed by what it meets.

Elementwise operations

#val (+) : (int, 's) t -> (int, 's) t -> (int, 's) t

a + b is the sum of a and b, wrapping on overflow as nx's integers do. Its type is the operands' common type, as for (-), (*), (/) and (mod).

#val (-) : (int, 's) t -> (int, 's) t -> (int, 's) t

a - b is the difference of a and b.

#val (*) : (int, 's) t -> (int, 's) t -> (int, 's) t

a * b is the product of a and b.

#val (/) : (int, 's) t -> (int, 's) t -> (int, 's) t

a / b is the quotient of a by b, truncated toward zero, and null where b is zero.

#val (mod) : (int, 's) t -> (int, 's) t -> (int, 's) t

a mod b is the remainder of a by b, of a's sign, and null where b is zero.

#val (+.) : (float, 's) t -> (float, 's) t -> (float, 's) t

a +. b is the IEEE 754 sum of a and b. Its type is the operands' common type, as for (-.), (*.), (/.) and (**).

#val (-.) : (float, 's) t -> (float, 's) t -> (float, 's) t

a -. b is the difference of a and b.

#val (*.) : (float, 's) t -> (float, 's) t -> (float, 's) t

a *. b is the product of a and b.

#val (/.) : (float, 's) t -> (float, 's) t -> (float, 's) t

a /. b is the quotient of a by b.

#val (**) : (float, 's) t -> (float, 's) t -> (float, 's) t

a ** b is a to the power b.

#val (=) : ('a, 's) t -> ('a, 's) t -> (bool, 's) t

a = b is true iff a and b are equal in talon's total order (Type.compare_value), so nan = nan and -0. = 0., and null if either is null. The operands meet at their common type.

#val (<>) : ('a, 's) t -> ('a, 's) t -> (bool, 's) t

a <> b is not (a = b).

#val (<) : ('a, 's) t -> ('a, 's) t -> (bool, 's) t

a < b is true iff a comes before b in talon's total order, so x > float 15. holds for NaN, and null if either is null. Operands of an extension type need a declaration made with ~ordered:true.

#val (>) : ('a, 's) t -> ('a, 's) t -> (bool, 's) t

a > b is b < a.

#val (<=) : ('a, 's) t -> ('a, 's) t -> (bool, 's) t

a <= b is a < b || a = b.

#val (>=) : ('a, 's) t -> ('a, 's) t -> (bool, 's) t

a >= b is b <= a.

#val (&&) : (bool, 's) t -> (bool, 's) t -> (bool, 's) t

a && b is Kleene's conjunction: false if either is false, else null if either is null. Both operands are computed.

#val (||) : (bool, 's) t -> (bool, 's) t -> (bool, 's) t

a || b is Kleene's disjunction: true if either is true, else null if either is null.

#val not : (bool, 's) t -> (bool, 's) t

not a is the negation of a, null where a is.

#val if_ : (bool, 's) t -> ('a, 's) t -> ('a, 's) t -> ('a, 's) t

if_ c a b is a where c is true and b where c is false or null. a and b meet at their common type.

#val is_null : ('a, 's) t -> (bool, 's) t

is_null a is true where a is null and false elsewhere. It is never null.

#val coalesce : ('a, 's) t list -> ('a, 's) t

coalesce es is the first of es that is not null, or null. es meet at their common type.

#val is_in : 'a list -> ('a, 's) t -> (bool, 's) t

is_in vs a is true iff a is the same key as one of vs (see Type.compare_value), so it is false, never null, where a is null: not (is_in vs a) is true there, whereas not (a = v0 || a = v1) is null. a's type must hold each of vs; an extension's values are encoded with its declaration.

#val cut : 'a array -> ('a, 's) t -> (int, 's) t

cut edges a is the bin of a among edges, as int64. The bin is i where edges.(i) <= a < edges.(i + 1) in talon's total order, so k + 1 edges bound k bins, each holding its lower edge, and adjacent bins share an edge. It is null where a is null, below the first edge, or at or above the last, which NaN is: cut [| 0.; 0.5; 1. |] x is 0 for 0.2, 1 for 0.5 and null for 1..

Each edge is a literal of a's type, which must hold it and may round it, so cut edges a = int i exactly where a >= edges.(i) && a < edges.(i + 1). As that type stores them, the edges must ascend strictly and hold no NaN; -0. and 0. are one edge. a's type is one that (<) orders. edges is copied.

Raises Invalid_argument if edges has fewer than two elements.

#val cast : 'b Type.t -> ('a, 's) t -> ('b, 's) t

cast ty a converts a's values to ty:

  • between bool, integer and float types: integers take exact values only, so a fractional, infinite or NaN float, or a value out of range, is a data error; floats round to nearest; bool takes 0 and 1 only, and gives 0 and 1;
  • between string and categorical types: the text is kept, and a categorical takes only the strings of its dictionary, and another string is a data error;
  • between datetimes of one clock, between durations, and between clocks: values are kept, and a value that is not a whole number of the new unit is a data error;
  • between clocks and durations: a time of day is its span from midnight, converted as between durations, and a duration outside [0, 1 day) is a data error for a clock;
  • between a duration and int64: the integer is the duration's ticks in its own unit, exactly, for every int64. To read another unit, cast between durations first, as cast Type.int64 (cast (Type.duration Ms) d) does for milliseconds;
  • between dates and wall-clock datetimes: a date becomes the datetime of its midnight, and a datetime becomes the day it falls on, so 1969-12-31T23:59:59 becomes 1969-12-31. A value out of the new type's range is a data error;
  • lists element by element, records with the same field names field by field, and tensors of the same shape element by element, as Nx.cast does.

Any other pair is a problem that names the function that converts it, if one does: Str.parse and Temporal.parse for text to values, and Temporal.format for dates, clocks and datetimes to text, and Temporal.wall or Temporal.localize between the clocks. An instant falls on a day only in a zone, so it casts neither to nor from a date. a needs a column type.

#type fn = {
  1. #f : 'a 'b. ('a, 'b, Nx.host) Nx.t -> ('a, 'b, Nx.host) Nx.t;
}

The type for elementwise nx functions of one argument that preserve its dtype, such as Nx.exp, on host values as columns hold.

#val nx : fn -> ('a, 's) t -> ('a, 's) t

nx { f } a applies f to a's values with nx's semantics, IEEE's for floats: nx { f = Nx.exp } x. a has an nx dtype: an integer, float or bool type, bool computing as Nx.bool. When the verb is applied, talon calls f once on a traced value of that dtype and records the operations f performs; an f that moves, reduces or reshapes its argument is a problem, and so is one that ignores it, which would lose its nulls.

#type fn2 = {
  1. #f2 : 'a 'b. ('a, 'b, Nx.host) Nx.t -> ('a, 'b, Nx.host) Nx.t -> ('a, 'b, Nx.host) Nx.t;
}

The type for elementwise nx functions of two arguments of one dtype that preserve it, such as Nx.atan2.

#val nx2 : fn2 -> ('a, 's) t -> ('a, 's) t -> ('a, 's) t

nx2 { f2 } a b is like nx for two arguments, which meet at their common type: nx2 { f2 = Nx.atan2 } y x.

Reductions

A reduction takes a row expression to one value per frame. Over no values, sum, count and n_unique are 0, collect is the empty list, and every other reduction is null.

#val rows : (int, agg) t

rows is the number of rows of the frame, as int64.

#val count : ('a, row) t -> (int, agg) t

count a is the number of non-null values of a, as int64.

#val sum : ('a, row) t -> ('a, agg) t

sum a is the sum of a's values: int64 over integers, a's type over floats and durations. Integer sums wrap as nx's integers do; a duration sum that overflows is a data error. Other types are a problem.

#val min : ('a, row) t -> ('a, agg) t

min a is the least of a's values in talon's total order, of a's type. An extension type needs an ordered declaration.

#val max : ('a, row) t -> ('a, agg) t

max a is the greatest of a's values, like min.

#val first : ('a, row) t -> ('a, agg) t

first a is a's first non-null value in frame order.

#val last : ('a, row) t -> ('a, agg) t

last a is a's last non-null value in frame order.

#val only : ('a, row) t -> ('a, agg) t

only a is a's one distinct non-null value, and a data error naming the frame when a has several.

#val mean : ('a, row) t -> (float, agg) t

mean a is the arithmetic mean of a's values, as float64: their sum divided by their count. Integers of at most 32 bits sum exactly in int64 in a frame of fewer than 231 values, and the sum rounds once to float64; other values sum in float64 in row order. a is an integer or float expression, as for std, var, median and quantile; other types are a problem.

#val std : ('a, row) t -> (float, agg) t

std a is the sample standard deviation of a's values, dividing by n - 1, and null for fewer than two values. It is infinite only where the standard deviation exceeds max_float. Its bits depend on the frame's values in row order alone. Over traced values, and under Rune.jit, it is a two-pass form, equal within rounding.

Raises Invalid_argument over a float64 operand under Rune.jit, or over traced float64 values under Rune.vmap, as that form reads them to detect overflow.

#val var : ('a, row) t -> (float, agg) t

var a is the sample variance of a's values, like std. It is infinite only where the variance exceeds max_float.

#val median : ('a, row) t -> (float, agg) t

median a is quantile 0.5 a.

#val quantile : float -> ('a, row) t -> (float, agg) t

quantile p a is the p-quantile of a's values, interpolating linearly between the two nearest ranks.

Raises Invalid_argument if p is not in [0;1].

#val n_unique : ('a, row) t -> (int, agg) t

n_unique a is the number of distinct keys of a, null being one key, as int64.

#val arg_min : ('a, row) t -> (int, agg) t

arg_min a is the zero-based position in the frame of a's first least value, as int64. An extension type needs an ordered declaration.

#val arg_max : ('a, row) t -> (int, agg) t

arg_max a is the position of a's first greatest value, like arg_min.

#val pivot : (string, row) t -> ('a, row) t -> (Record.t, agg) t

pivot key v is, per frame, one record with a field per category of key's categorical type, in dictionary order, named as the category. The field holds v at the frame's row whose key is that category, and is null where the frame has no such row. A null v in a cell is a null field, so a missing cell and a null value look alike; a bool true pivoted beside v tells them apart.

The record's type, and the plan's schema, follow from key's type alone and never from its values: cast key to the categorical naming the fields, or read them from the data with Kit.categorize. Every non-null value of v lands in a cell of its row, or the run fails: a row with a null key and a null v holds no cell, so a left join's unmatched row leaves its frame's fields null; a null key with a non-null v has no field to hold the value and is a data error, as is any value outside the dictionary at the cast. Two rows of one frame with one key are a data error; reduce them first with Query.aggregate on the frame's keys and key. Filtering before pivot removes the groups left with no row.

A data error reports the failing frame's first row, naming the key.

Cost: O(n + g k) for n rows, g frames and k categories. Under over or window a frame's record holds its k cells, so the rows together hold their frames' k cells.

Its problems are a key whose type is not categorical, and a v with no column type, or none reachable. aggregate with it blocks, holding the columns it reads. Raises Invalid_argument under Rune.jit, and under Rune.vmap over traced keys (Compiled, mapped and differentiated runs): its failures read them.

#val collect : ('a, row) t -> ('a array, agg) t

collect a is a's values in frame order as a list of a's type, with one element per row of the frame and a null element for a null value. It is never null, and is the empty list over no rows. a must have a column type, and an extension column is read with each.

Cost. O(n). Under over each row holds its partition's list, and the lists together hold the sum of the partitions' squared sizes. Under window every row holds its window's list, and memory holds them all together.

Raises Invalid_argument under Rune.jit (Compiled, mapped and differentiated runs).

Frames

over evaluates an expression in each partition of the enclosing frame, the rows whose columns by are the same keys (Type.compare_value), each partition ordered by order. A reduction is broadcast over its partition's rows. A row expression such as shift or rank runs within its partition and returns its values to their rows.

#val over : ?by:string list -> ?order:Order.t list -> ('a, 's) t -> ('a, row) t

over ~by ~order e is e evaluated in each partition of the enclosing frame (Frames). Its type is e's. by defaults to no columns, which is one partition. order defaults to none, which is the frame's order.

At the top of derive or filter the frame is the whole input, so over (mean x) is the input's mean and it blocks the pipeline (Query).

Its problems are a column named twice in by or in order, and an order key on a column whose type is or holds an extension type.

Raises Invalid_argument over traced columns (Compiled, mapped and differentiated runs) if by is nonempty.

#val shift : int -> ('a, row) t -> ('a, row) t

shift n a is a's value n rows earlier in the frame, or -n rows later if n is negative, and null past the frame's edges.

#val rank : ('a, row) t -> (int, row) t

rank a is the 1-based rank of a's value among the frame's non-null values in talon's total order, ties taking the lowest rank (SQL's RANK), as int64, and null where a is null. An extension type needs an ordered declaration.

Windows

A window is, at each row of a frame, a frame of the rows around it, and a reduction over it gives the row's value. Each row has a key, the value of an expression on or, without on, the row's place in the frame from 0. Row i's window holds the rows of its frame whose key k has ki - before < k <= ki + after in talon's total order, where ki is row i's key and the bounds before and after are expressions of the frame, so they may differ by row. At a row whose key or bound is null, the value is null. Every window leaves out the rows whose key is null.

#val window : ?on:('k, row) t -> ?before:('d, row) t -> ?after:('d, row) t -> ?exclude_key:bool -> ('a, agg) t -> ('a, row) t

window ~on ~before ~after ~exclude_key r is, at each row, r over the row's window (Windows). Its type is r's. Without before the window has no lower limit, and after defaults to 0. With ~exclude_key:true it leaves out the rows whose key is ki, which without on is the row itself.

window (sum x) is the running sum, and window ~on:ts ~before:(span (Time.Span.days 7)) ~exclude_key:true (sum x) sums the seven days before ts. Without on the key is the row's place, so over ~order sets it, as in over ~order:[ Order.asc "day" ] (window ~before:(int 28) (mean x)), the mean of a day and the 27 before it.

It blocks the pipeline as over does. Keys and bounds and Float keys and results give its keys and bounds, and Cost its cost.

Its problems are an on of another type than Keys and bounds allows, a bound of another kind, type or unit, and over, shift, rank or window inside r, outside a per_list's body.

Keys and bounds

on is an integer, a float, a date, a datetime, a duration or a clock. A bound is a number of on's kind, a duration of on's unit or a coarser one for a temporal on, and an integer without on. A window holds its rows in key order, the rows of one key in the frame's order, which over ~order sets.

ki - before and ki + after compute in the type where on and its bounds meet. They are exact for integers, and for temporal keys, which compute in ticks of on's unit (seconds for a date). An exact result past the type's range lies below or above every key.

Float keys and results

Float bounds follow IEEE 754 and round once, and NaN is the greatest key, so that:

  • an infinite or NaN key's window is empty under finite bounds, and without before holds every key up to its own;
  • at a finite key, ~before:(float infinity) reaches every key above neg_infinity and ~after:(float infinity) every key up to infinity;
  • a NaN before holds no key, and a NaN after every key from the window's lower limit on, NaN included.

A float sum's bits depend on the window's rows, their places among the rows of the enclosing frame, all partitions included, and whether the window has before or ~exclude_key:true with after. They may differ from Query.aggregate's over the same rows. With ~exclude_key:true and after, var and std are Query.aggregate's over the window's rows. Otherwise, with before, they come from Nx.moments_ranges over the enclosing frame's values, so that their bits depend on the window's values and their places among them, and without before they may differ from both.

Cost

With on, the frame's rows are sorted once by key. only, the arg_ reductions, and min and max of values other than integers and booleans also sort their values. Without before, count, sum, mean, first, last, var and std then take one scan, O(n) for n rows, and min, max, only and the arg_ reductions take that scan too. With before, all of these take O(n log n), as var and std without it do over float64 values whose variance overflows or underflows. median, quantile and n_unique reduce every window's rows, in time proportional to the windows' total size, which is O(n2) without before. They hold in memory at most 220 of those rows, or one window's rows when a window has more. A window with ~exclude_key:true and an explicit after costs in all of these as one with before, but its var and std reduce every window's rows, as median does.

Lists

A list has one position per element, and a null list none, so its length is 0. A row's lists are read over a frame with one row per position, in list order. An output's column holds the list's element at the position, and every other column of the enclosing frame the row's value. Several outputs are read in lockstep, so position j holds element j of each, and lists of one row must have one length. Frames nest, so a per_list of an element that is itself a list reads that inner list. rows over a list's frame is the list's length. over, shift, rank and window run within the list, and each must read an output of its list.

#val per_list : row out list -> ('a, agg) t -> ('a, row) t

per_list ls r is, at each row, r over the frame of the elements of the lists that the outputs ls compute (Lists). Its type is r's. A list's norm is:

ocaml
let xs = Col.v Kind.(list float) "xs" and x = Col.float "x"
let norm = per_list [ "x" := xs ] (sum (x *. x)) ** float 0.5

per_list [ "t" := spikes ] (mean (t -. shift 1 t)) is a spike train's mean interval. Nulls and floats gives its nulls and float results, and Cost its cost.

Where its outputs hold no over, shift, rank or window, its value at a row reads that row alone, so it streams. It may stand in a window's operand. A failure in r is at the row of its list.

Its problems are a hiding output (Outputs), an output that is not a list, no output, two outputs of one name, and an over, shift, rank or window in r that reads no output.

Raises Invalid_argument under Rune.jit when it reads a row's list, and under Rune.vmap where the lists' offsets or validity are traced (Compiled, mapped and differentiated runs).

Outputs

An output must not hide a column of the enclosing frame other than a list it outputs unchanged, since r could not read the hidden column. keep therefore outputs a list under its own name, and r reads its element there.

An over, shift, rank or window that reads only the row's values, which do not change along the list, would look like the enclosing frame's, so it is a problem.

Nulls and floats

A row where an output is null has no frame, whatever the lengths of its other lists, so r does not run there, and the value is null, that of count and rows included. Lists of one row whose lengths differ are a data error at that row when none of them is null. An empty list's frame has no rows, so reductions give their value over no values (Reductions). A null element is a null value of the frame, which a reduction reads as it reads any null.

A float sum adds a list's values in list order, a float mean sums them in float64 in list order and divides by their count, and var and std sum squared deviations from the list's mean in list order, less the term that takes out the mean's rounding.

Cost

A reduction in r costs O(n + e) for n rows and e elements, as a reduction of e rows costs, and a window costs as Cost states. median, quantile, rank, a window ~on and an over ~order in r sort, and a column of the enclosing frame that r reads is copied once per element.

Records

A record expression computes a value of a Type.t.Record type from named expressions of the record's shape. A record and its fields have separate nulls, and is_null of a record tests the record alone.

#val list : ('a, 's) t list -> ('a array, 's) t

list es is the list of es' values at each row, in order, as a list of the type where the types of es meet (Type.common), with a null element for each null value. It is never null: list [ x; null ] where x is null is a list of two null elements. A literal or null element takes that type, and store gives one where nothing does. A list operand stays one element, so a list of lists nests and never flattens.

It computes under Rune.jit and Rune.vmap, since the lists' sizes follow from the columns' lengths, but reading a list raises under Rune.jit as for any list column: in a result, and under per_list and Query.unnest (Compiled, mapped and differentiated runs).

Its problems are an empty es, and operands that do not meet.

#val record : 's out list -> (Record.t, 's) t

record os is the record of the outputs os, its fields named as they are, in order: record [ "x" := x; keep (Sel.names [ "flux" ]) ]. It is never null, even where every field is. if_ c (record os) null is null where c is not true.

Its problems are those of binding os, and two outputs of one name.

#val field : 'a Kind.t -> string -> (Record.t, 's) t -> ('a, 's) t

field k n r is the field n of r read as k, null where r is null or the field is: field Kind.float "flux" band. k binds the field as a Col handle binds a column, so field reads fields of types without an extension, and within reads any.

Its problems are a field that r lacks and one that k does not read.

#val within : (Record.t, row) t -> 's out -> 's out

within r o is the outputs o bound over r's fields, so that inside o a name reads the field of that name and no column of the frame, null where r is null or the field is. within r (keep Sel.all) spreads a record, within r (each Sel.all { column = (fun n x -> "b_" ^ n := x) }) renames, and within r ("snr" := Col.float "flux" /. Col.float "err") computes. To combine a field with a column of the frame, read the field with field outside within.

Its problems are those of binding o against r's fields, a name that r lacks included, an r that is not a record, and, under Query.derive and Query.unnest, an output named as a column of the input, which would replace that column.

OCaml values

These functions compute in OCaml, once per row, at native speed. They must be pure; an exception they raise propagates from the run.

#val const : 'a -> ('a, 's) t

const v is v on every row. It is typed by what it meets, an extension's value being encoded with its declaration; a type that does not hold v is a problem.

#val ($) : ('a -> 'b, 's) t -> ('a, 's) t -> ('b, 's) t

f $ a applies f's function to a's value, once per row where no argument is null, and is null elsewhere: const mk $ carrier $ delay. An argument is decoded with its column type, so it needs one, unless it is itself an OCaml value: a const, a $ result or an option. The result is typed by what it meets, or by store.

#val option : ('a, 's) t -> ('a option, 's) t

option a is Some v where a is v and None where a is null. It is never null, and is an argument of ($) or what Query.values decodes. a needs a column type.

#val of_option : ('a option, 's) t -> ('a, 's) t

of_option a is v where a is Some v and null where it is None. It is typed by what it meets, or by store.

#val store : 'b Type.t -> ('b, 's) t -> ('b, 's) t

store ty a is a typed as ty, as if a met an operand of type ty: a literal, null, const, $ or of_option result takes ty, and an a of a type that ty contains is widened to it. A literal or const value that ty does not hold is a problem, and a $ or of_option result that it does not hold a data error.

Text

#module Str : sig ... end

Text.

Time

#module Temporal : sig ... end

Temporal values.

Hashing

hash is the xxHash64 of each row's value, in the xxHash specification's sense, with a seed. Its result is the 64-bit hash shifted right by two bits, so it lies in [0;max_int] and mod keeps it non-negative. From 1.0 the definition below is a frozen format, so a split made with one release holds in every later one.

A value's input to the hash is decided by what its kind reads, whatever its storage:

  • a boolean is the integer 0 or 1, and an integer its value as a 64-bit two's complement word, so every width of one number hashes alike and a uint64 beyond int64 hashes as the word it stores;
  • a float is its float64 value's bits, -0. as 0. and every NaN as the word 0x7FF8000000000000;
  • a date is its int64 days from 1970-01-01, and a clock, a duration or a datetime its nanoseconds, wrapped to 64 bits;
  • a word above is hashed as eight little-endian bytes; a string, a categorical's text and a binary as its bytes;
  • a null at any depth hashes the eight bytes of the word 0x9E3779B185EBCA87;
  • a record hashes, as one input, its fields' 64-bit hashes, before the shift, as eight little-endian bytes each, in field order, with field names left out.

The seed is the OCaml int sign-extended to 64 bits, 0 by default, the same at every depth of a record. hash over a list, a tensor or an extension is a problem. Exact casts within a kind keep the hash, so hash (cast ty a) = hash a for them.

#val hash : ?seed:int -> ('a, 's) t -> (int, 's) t

hash ~seed a is the hash of each row of a (Hashing), as int64, and is never null. A column whose bytes nx cannot read, a traced one other than a categorical, raises Invalid_argument naming Expr.hash (Compiled, mapped and differentiated runs).

Formatting

#val pp : Stdlib.Format.formatter -> ('a, 's) t -> unit

pp ppf e formats e as it is written inside Expr.( … ), with fewest parentheses: (amount -. over (mean amount)) /. over (std amount). Besides:

  • a handle formats as its column name, quoted as an OCaml string unless it is an OCaml lowercase identifier that is neither a keyword nor a value of this module;
  • a literal formats as its value: 15, 15., nan, "text", true, 2024-03-15, 2024-03-15T09:30:00, 15m;
  • an OCaml value of const formats as <const>, and the functions of nx and nx2 as <fn>;
  • an is_in list and cut edges format with their operand's type once bound, and as […] before.