BLOGE
0.9.8-RC1在线版 · 事实校验 2026-09-15 · English
第 4 章 —— 流动的数据
承诺: 读完本章后,你会理解数据如何进入 、穿过并离开一个 BLOGE graph;同时你也会知道什么时候该用 input binding、什么时候该用 transform、什么时候必须写成 operator。
学习目标
- 解释
input块中可用的三类数据来源:context 值、上游输出 和 字面量。 - 使用 路径表达式 访问嵌套字段,包括安全导航(
?.)和空值合并(??)。 - 用 下标访问 读取列表与映射元素(
xs[i]、xs?[i]、xs[-1]、m["key"]),并在表达式结果上使用 动态成员访问((expr).field、(expr)?.field)。 - 区分 transform(不调度 Operator 的数据投影)与 operator node(可独立调度的业务逻辑)。
- 在输入绑定中使用 lambda 表达式 做集合操作(
map、filter、reduce)。
前置条件
源示例
| 文件 | 展示内容 |
|---|---|
order-enrichment-lambda.bloge | 在输入绑定中使用 lambda 做集合处理 |
order-summary-index-access.bloge | 下标访问、负索引与动态成员访问 |
basic.bloge (transform) | 最小 transform 块 |
safe-navigation.bloge | 安全导航 ?. 与空值合并 ?? |
lambda-map.bloge | 在输入绑定里使用 map |
order-process.bloge | 真实工作流里的 transform |
为什么这很重要
第 3 章里你学到的是“依赖决定什么时候运行”。但依赖只决定 何时,不决定 拿到什么数据。
数据流设计得好,graph 会清晰、易维护;数据流设计得差,每个节点都在做额外的字段搬运和结构重塑,最后谁也说不清每一步真正的业务职责是什么。
BLOGE 为数据处理提供了分层工具:
| 层次 | 工具 | 调度成本 | 适用场景 |
|---|---|---|---|
| 路由 | Input binding(input { … }) | 无 —— 在装配阶段求值 | 传递 context 字段或上游输出 |
| 投影 | Transform(transform { … }) | 无 —— 虚拟节点,不调度 operator | 重组、重命名字段,但不包含业务逻辑 |
| 计算 | Operator node(node … : Op { … }) | 完整调度成本 | 业务逻辑、I/O、副作用 |
选对层次,graph 才能既快又好读。
心智模型
- Input bindings 从
ctx或someNode.output.field取值。 - Transforms 是虚拟的——引擎不会为它们单独开线程去调度。
- Operator nodes 才是真正执行代码的单元,因而会附带 timeout、retry、fallback 等运行时语义。
第一个可运行示例
Input bindings —— 基础形式
每个 input { … } 都是一组 key-value 绑定。右侧是一个表达式,会在输入装配阶段被求值。
node calcPrice : CalcPriceOperator {
input {
user = fetchUser.output // whole output object
products = fetchProducts.output // whole output object
userId = fetchUser.output.userId // nested field
tax = ctx.taxRate // context value
label = "order" // literal string
}
}
三个来源,同一个 block:
| 来源 | 语法 | 例子 |
|---|---|---|
| Context | ctx.fieldName | ctx.taxRate |
| 上游输出 | nodeName.output 或 nodeName.output.field | fetchUser.output.userId |
| 字面量 | 字符串或数字字面量 | "order"、42 |
路径表达式
路径表达式用来访问嵌套结构:
input {
city = fetchUser.output.address.city
}
这表示沿着 output → address → city 逐层读取。
安全导航与空值合并
当某个字段可能为 null 时,可以用 安全导航运算符(?.)避免空指针,再用 空值合并运算符(??)给一个默认值:
// From safe-navigation.bloge
node normalize : Normalize {
input {
profileName = ctx.request?.user?.name
result = fetch?.result?.value ?? "missing"
names = ctx.items?.map(x -> x.profile?.name)
}
}
(摘自 safe-navigation.bloge。)
| 运算符 | 含义 |
|---|---|
?. | 如果左边是 null,就停止继续向下访问并返回 null |
?? | 如果左边为 null,就改用右边的默认值 |
它们可以自然组合:a?.b?.c ?? "default" 的意思就是“尝试读取 a.b.c;如果中途任意一层是 null,就返回 default”。
访问列表与映射
路径表达式访问的是 字段。要访问列表的 元素 或映射的 条目,使用下标访问运算符:
| 语法 | 含义 |
|---|---|
xs[i] | 取下标 i 位置的元素;越界时抛出运行时错误 |
xs?[i] | 安全下标访问;越界时返回 null 而不抛错 |
xs[-1] | 负下标从尾部计数(-1 即最后一个元素) |
m["key"] | 按字符串 key 查找映射条目 |
(expr).field | 在任意表达式结果上读取字段 |
(expr)?.field | 在表达式结果上做安全字段访问 |
下标访问可与已学过的安全导航、空值合并、lambda 等自由组合,并且仍然是在输入装配阶段被求值,不引入额外的调度开销:
// From ch04/order-summary-index-access.bloge
node fetchOrders : FetchOrdersOperator {
input {
customerId = ctx.customerId
}
}
transform summary {
firstOrderId = fetchOrders.output.recentOrderIds[0]
lastOrderId = fetchOrders.output.recentOrderIds[-1]
safeFirst = fetchOrders.output?.recentOrderIds?[0] ?? "none"
regionLabel = ctx.regionMap["north-east"]
totalOrders = fetchOrders.output.totalOrders
}
常见陷阱 ——
xs[i]还是xs?[i]?两种写法都能取第
i个元素,但在列表比预期短的时候行为完全不同:
xs[i]是 严格的:越界即报错,求值立刻失败。 当“空列表 / 短列表”本身就是 bug 时使用——例如读取刚刚创建出来的订单。xs?[i]是 宽容的:越界返回null,配合??给出兜底值。 当“暂时没有数据”是正常情形时使用——例如读取可能尚未写入的历史记录。默认优先选择严格写法;只有在确实需要容忍缺失数据时才换成安全写法。
跟随一笔订单经过四种数据形状
不要逐个背表达式符号,而要跟着一个业务对象走。订单从 GraphContext 里的
几个标识开始,经两个 Operator 补充事实,成为计价输入,最后被投影成供后续
步骤读取的小型摘要。