You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Add lambdas and variables for higher-order scalar functions - #10502
Adds lambdas, the function arguments of higher-order scalar functions such as list_transform (#10503). A lambda is a list of parameter names and a body that reads them through variables. A function that takes a lambda keeps it in its options, binds it once when its node is bound, and runs the bound lambda over values it makes from its inputs.
Variables
var("x") builds Expression::Variable("x"): a leaf, like $, that reads the parameter x of the enclosing lambda.
It displays as @x, so @x (a parameter) and $.x (a field of the input) never look alike.
A variable exists only in a written body. Binding the lambda turns @x into $.x, a field of the struct of the lambda's inputs (see below), so BoundExpression is unchanged.
Anywhere else it fails to bind, type, apply and serialize with "variable @x is not a parameter of an enclosing lambda".
Lambdas
let lambda = Lambda::new(["x"],checked_add(var("x"),lit(1_i32)))?;// (x) -> (@x + 1i32)let bound = lambda.bind(&[DType::Primitive(PType::I32,NonNullable)])?;// (x) -> ($.x + 1i32): an ordinary bound expression over the struct {x: i32}
Lambda is the lambda as written: params() and body(), an Expression.
Lambda::new rejects a repeated parameter, and a body that reads from outside the lambda: $ or a column, another lambda's parameter, or the scan's row_idx() and row_count(). Constants are fine, such as the literal set in x -> list_contains([2, 3], x).
bind(param_dtypes) turns the lambda into an ordinary expression over a struct of its inputs, one field per parameter: each @x becomes $.x, and the body is bound to that struct. It returns a BoundLambda.
return_dtype(param_dtypes) gives the body's dtype over the same struct, without binding it, the way Expression::return_dtype does for a whole expression.
BoundLambda is the lambda bound: params() with their dtypes, body(), an ordinary BoundExpression over the struct, dtype(), the body's, and is_bound_to(param_dtypes).
BoundLambda::evaluate(args, len) puts one array per parameter in that struct and applies the bound body to it. The arrays must have the dtypes the lambda is bound to. It binds nothing, and like any applied expression, the result is computed later.
is_infallible(), on both, says whether nothing in the body can fail, so a function knows whether the body may run on values whose results are never read, such as dictionary values no row uses.
Using a lambda in a function
A function's options are an enum of the lambda as written and as bound. Its children are its ordinary inputs.
enumMyFnOptions{Lambda(Lambda),Bound(BoundLambda),}implScalarFnVTableforMyFn{typeOptions = MyFnOptions;fnbind_options(&self,options:&MyFnOptions,args:&[DType]) -> VortexResult<Option<MyFnOptions>>{match options {MyFnOptions::Lambda(lambda) => Ok(Some(MyFnOptions::Bound(lambda.bind(/* param dtypes, from args */)?))),MyFnOptions::Bound(lambda) => {vortex_ensure!(lambda.is_bound_to(/* param dtypes, from args */),"bound to other dtypes");Ok(None)}}}fnexecute(&self,options:&MyFnOptions,args:&dynExecutionArgs,ctx:&mutExecutionCtx) -> VortexResult<ArrayRef>{letMyFnOptions::Bound(lambda) = options else{vortex_bail!("not bound")};
lambda.evaluate(vec![/* the parameters' arrays, made from args */], len)}}
ScalarFnVTable::bind_options(options, arg_dtypes) is new, hidden from the docs, and does nothing by default. It runs whenever a bound node is built, and whenever a ScalarFnArray is built from its children, before return_dtype. That's where a function binds its lambda, once, so execute finds it bound.
A rebuilt node calls bind_options again, possibly with other dtypes, so a function checks that options already bound still fit, and errors if they don't. No rewrite today changes the dtype a lambda is bound to.
An Expression holds the written lambda, and a BoundExpression or an array holds the bound one.
The body reads only its parameters, so the function's children are everything it reads. Binding, traversal, replace, field pruning and pushdown see an ordinary node and never look inside the lambda.
Lambdas nest: a call that takes a lambda is an ordinary node inside another lambda's body. Its children are part of the outer body, so they can read the outer parameters, and its own lambda reads only its own. Binding the outer lambda binds the inner one.
System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.
Some benchmarks with significant performance changes were compared across different runner types, so the changes might not come from the code. Run base and head on the same runner type, or use CodSpeed Macro Runners for more stable results.
The flamegraph wasn't generated: the symbols of the profiling trace could not be resolved, because no information about the language frames was collected.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds lambdas, the function arguments of higher-order scalar functions such as
list_transform(#10503). A lambda is a list of parameter names and a body that reads them through variables. A function that takes a lambda keeps it in its options, binds it once when its node is bound, and runs the bound lambda over values it makes from its inputs.Variables
var("x")buildsExpression::Variable("x"): a leaf, like$, that reads the parameterxof the enclosing lambda.@x, so@x(a parameter) and$.x(a field of the input) never look alike.@xinto$.x, a field of the struct of the lambda's inputs (see below), soBoundExpressionis unchanged.Lambdas
Lambdais the lambda as written:params()andbody(), anExpression.Lambda::newrejects a repeated parameter, and a body that reads from outside the lambda:$or a column, another lambda's parameter, or the scan'srow_idx()androw_count(). Constants are fine, such as the literal set inx -> list_contains([2, 3], x).bind(param_dtypes)turns the lambda into an ordinary expression over a struct of its inputs, one field per parameter: each@xbecomes$.x, and the body is bound to that struct. It returns aBoundLambda.return_dtype(param_dtypes)gives the body's dtype over the same struct, without binding it, the wayExpression::return_dtypedoes for a whole expression.BoundLambdais the lambda bound:params()with their dtypes,body(), an ordinaryBoundExpressionover the struct,dtype(), the body's, andis_bound_to(param_dtypes).BoundLambda::evaluate(args, len)puts one array per parameter in that struct and applies the bound body to it. The arrays must have the dtypes the lambda is bound to. It binds nothing, and like any applied expression, the result is computed later.is_infallible(), on both, says whether nothing in the body can fail, so a function knows whether the body may run on values whose results are never read, such as dictionary values no row uses.Using a lambda in a function
A function's options are an enum of the lambda as written and as bound. Its children are its ordinary inputs.
ScalarFnVTable::bind_options(options, arg_dtypes)is new, hidden from the docs, and does nothing by default. It runs whenever a bound node is built, and whenever aScalarFnArrayis built from its children, beforereturn_dtype. That's where a function binds its lambda, once, soexecutefinds it bound.bind_optionsagain, possibly with other dtypes, so a function checks that options already bound still fit, and errors if they don't. No rewrite today changes the dtype a lambda is bound to.Expressionholds the written lambda, and aBoundExpressionor an array holds the bound one.replace, field pruning and pushdown see an ordinary node and never look inside the lambda.Not supported yet
x -> x + $.a.