arrow expression
Expression,顾名思义,是 arrow 中计算的表达式。这里可以通过 Substrait 来构建 Plan 或者单机的表达式。
Glance
在 Arrow 中,Expression 可以分为下面几种可能的形式:
- Call
- Function + Args 的包装,分为 Bounded / Unbounded 的类型
- Parameter (
A reference to a single (potentially nested) field of the input Datum.
)- Arrow 或者 Input 中 Field 的包装,分为 Bounded / Unbounded
- 可以通过
FieldRef
之类的来构建。用户大部分时候只走field_ref
,但底下实现还是个Parameter
- Literal: 正常的 Literal, 包含 Null。实际上由
Datum
(我们在介绍 Compute 的时候讲过) 实现
上面这几套接口在 Expression 中表现为比较有趣的形式:
1 | /// An unbound expression which maps a single Datum to another Datum. |
这里的使用方式很有意思,类似 enum:
1 | auto call = expr->call(); |
Datum
作为 literal
我们之前就已经介绍过了(囧)
它自己还有整个 Expression 的类型 (Type
),然后此外,整个 Expression
还有对应的 Hash 和 Equal 的方法,用来组一些比较。我们之后会看到这些方法
1 | std::string ToString() const; |
此外,这里还有一些特殊的属性,需要额外提供一下:
IsBound
:- 这个后面讲,有点复杂
IsScalar
(需要注意的是,在这里,complex type 之类的也算是 scalar)- 对于
Datum
来说,Datum 包含的是否是 Scalar (它还能包含 Array Table RecordBatch ChunkedArray 之类的) - 对于 FieldRef,这里…啥都行!
Call
: all argumentIsScalar
, and function type isSCALAR
.
- 对于
FieldRef and Parameter
FieldRef
是个很奇怪的东西,表示对某个 Field 的引用,它本身也可以是:
1 | /// Unlike FieldPath (which exclusively uses indices of child fields), FieldRef may |
他可以表示的路径如上所述, 这里可以用名字表示。那么实际上内部用 FieldPath
是最好的,但是这里 xjb 糊了一套,用户 Bind 的时候要传个 Schema 进来,然后用这个 Schema 来 Bind,Bind 完里面还是原来那个 "a.b.c"
,只是绑定了一些类型。
在 Bind 的时候,注意到这些 Post Bind,类型是在 Bind 之后绑定的(还是个 TypeHolder
呢,呵呵)
1 | struct Parameter { |
Call
Call 表示一个 fn call.
如果是 Call
, 这里会有对应的参数:
1 | struct Call { |
这里可以通过 function
来判断是否做 binding。这里的含义还是比较清晰的,Expression 的主要内容还是在这个地方应该是最重要的一个类型。+
-
之类的,本身有 add
之类的 Function ( 在之前的 compute 模块介绍过 )。而 Expression
层会组织成:
1 | Call(function_name="Add", arguments={field_ref("a.b"), literal(1000)}) |
这样的形式,在 Bind
以后,这里会绑定到 Function
和对应的 kernel
执行器上。
这里还提供了 and
, or
之类的东西给用户,非常有意思:
1 | ARROW_EXPORT Expression project(std::vector<Expression> values, |
比较有意思的是 project
,会产生一个新的 struct
类型。
Bind
对 Expression 的 Bind 会比较复杂. 这里可能做的事情有:
- Bind 所有的子成员,然后进入
BindNonRecursive
- 拿到所有 arguments 的类型
- 根据 Name 和参数去找到对应的
Function
,Kernel
,这里先找完全匹配的 (`DispatchExact
)- 如果找到正好匹配的,就用这个
- 如果有
insert_implicit_casts
,就会尝试修改类型- 对于 Literal,找到 Literal 的最小类型(eg: 如果是
Datum(int32(8))
, 可以改成Datum(int8(8))
- 尝试去
DispatchBest
,然后如果有不一样的,- 如果是
field_ref
,直接用目标类型 - 如果是
field_ref
或者call
,插入一个 Cast
- 如果是
- 对于 Literal,找到 Literal 的最小类型(eg: 如果是
- 初始化
KernelContext
和KernelState
优化
表达式的很大一部分逻辑在于对表达式进行处理。下面列举了一组相关的 API。
1 | /// Weak canonicalization which establishes guarantees for subsequent passes. Even |
tools
arrow/compute/util.h
等地方提供了一组靠谱的工具,最典型的是一个 tree visitor:
1 | /// Modify an Expression with pre-order and post-order visitation. |
这个能够被用来扫 + 更新整个 tree.
Canonicalize
尝试把表达式处理成长得差不多的情况。
e.g: 对同样的可交换操作的处理
1 | ((a + b) + 2) + 3 |
这里发现是同样的操作,就会尝试整理,然后做成便于 constant folding 的形式。这里也会有 field_ref
, literal
, null literal
位置的关系。
e.g.: 对比较的处理
1 | a > 3 |
这里会被处理成一样的形式。
Others
这里还有 Constant Folding,抽出 Key-Values 等形式。其实这个表达式相对来说还是太 trivial 了,做个入门还行,难一点还是别看这个了。