在 No-Code 工具日益普及的今天,非技术用户需要一种安全、简单且可移植的方式来表达数据操作逻辑。Elo 语言应运而生 —— 这是一个纯函数式数据表达式语言,专门为 No-Code 工具设计,能够将同一表达式编译为 JavaScript、Ruby 和 PostgreSQL SQL,确保前端、后端和数据库层使用完全一致的语义。
设计哲学:安全第一的可移植表达式
Elo 的核心设计理念围绕三个关键词展开:简单、安全、可移植。与传统的通用编程语言不同,Elo 专注于数据表达式的特定领域,这使得它能够在保持强大功能的同时,大幅降低学习门槛。
安全性设计体现在多个层面。首先,Elo 采用值语义而非引用语义,所有数据都是不可变的,这从根本上避免了副作用带来的复杂性。其次,语言内置了 Finitio-like 的模式验证系统,开发者可以定义数据模式并在运行时进行验证:
let Person = { name: String, age: Int(c | c > 0) } in data |> Person
这种模式定义不仅提供了类型安全,还能自动进行数据转换和验证。对于非技术用户来说,这意味着他们可以在不担心数据格式错误的情况下编写表达式。
可移植性是 Elo 的另一大亮点。同一个 Elo 表达式可以无缝编译到三个不同的目标环境:
2 ^ 10 > 1000 and TODAY >= SOY
- JavaScript:
Math.pow(2, 10) > 1000 && DateTime.now().startOf('day') >= DateTime.now().startOf('year') - Ruby:
2 ** 10 > 1000 && Date.today >= Date.today.beginning_of_year - SQL:
POWER(2, 10) > 1000 AND CURRENT_DATE >= DATE_TRUNC('year', CURRENT_DATE)
这种跨平台一致性对于 No-Code 工具至关重要,因为这些工具往往需要在不同的技术栈中部署相同的业务逻辑。
多目标编译架构:AST→IR→目标代码
Elo 的编译器采用经典的三阶段架构,但针对多目标编译进行了特殊优化。整个编译流程可以概括为:解析 → 类型推断 → IR 生成 → 目标代码生成。
1. 解析与 AST 构建
Elo 使用手写的递归下降解析器,将源代码转换为抽象语法树(AST)。AST 节点设计简洁,主要包含以下几种类型:
- 字面量节点:数字、字符串、布尔值、时间值等
- 变量节点:引用外部数据或局部变量
- 二元操作节点:算术、比较、逻辑运算
- 函数调用节点:标准库函数或用户定义的 lambda
- 管道操作节点:Elixir 风格的
|>操作符
时间类型的处理是 Elo 的一大特色。语言原生支持 ISO8601 格式的时间字面量:
D2024-01-15 # 日期
D2024-01-15T10:30:00Z # 日期时间
P1D # 1天持续时间
PT1H30M # 1小时30分钟持续时间
2. 类型系统与 IR 转换
Elo 的类型系统基于 Hindley-Milner 类型推断的简化版本。编译器在 AST 到中间表示(IR)的转换过程中进行类型推断,确保表达式的类型一致性。
IR 层是 Elo 实现多目标编译的关键。它提供了一个与具体目标语言无关的中间表示,包含了所有必要的语义信息但去除了语法细节。IR 的设计原则包括:
- 平台无关性:不包含任何特定于 JavaScript、Ruby 或 SQL 的构造
- 语义完整性:保留所有必要的类型信息和操作语义
- 优化友好:支持常量折叠、死代码消除等优化
类型选择器(Type Selectors)是 Elo 类型系统的重要组成部分。它们允许开发者在运行时进行类型转换和验证:
Int("42") # 将字符串转换为整数,失败时抛出错误
Float(3.14) # 显式指定浮点数类型
Date("2024-01-01") # 解析日期字符串
3. 目标代码生成
针对每个目标语言,Elo 提供了专门的代码生成器。这些生成器将 IR 转换为目标语言的代码,同时处理语言间的语义差异:
JavaScript 生成器:
- 使用
Math.pow()处理幂运算 - 依赖 Luxon 库处理时间类型
- 生成 ES6 模块格式的代码
Ruby 生成器:
- 使用
**操作符处理幂运算 - 依赖 ActiveSupport 处理时间类型
- 生成 Ruby 方法或 lambda 表达式
SQL 生成器:
- 使用
POWER()函数处理幂运算 - 使用 PostgreSQL 的原生时间类型
- 生成可嵌入 SQL 查询的表达式
每个生成器都包含一个前导代码(prelude)系统,用于注入必要的运行时依赖。开发者可以选择是否包含前导代码,这在某些集成场景中非常有用。
时间处理:原生支持与跨平台一致性
时间处理是数据表达式中最复杂也最容易出错的部分之一。Elo 通过原生支持时间类型,彻底解决了这个问题。
时间字面量与操作
Elo 的时间字面量语法直观且符合 ISO8601 标准:
let
signup = D2024-06-15
in
TODAY > signup + P30D # 检查今天是否在注册日期30天后
时间操作符的设计考虑了常见业务场景:
+/-:时间与持续时间的加减...:时间范围(包含两端)in:检查时间点是否在范围内
跨平台时间库抽象
为了确保不同平台间的时间处理一致性,Elo 定义了一个抽象的时间库接口。每个目标语言的实现都需要提供对应的适配器:
- JavaScript:使用 Luxon 库,通过
DateTime和Duration类 - Ruby:使用 ActiveSupport,通过
Date、DateTime和ActiveSupport::Duration类 - SQL:使用 PostgreSQL 的原生时间类型和函数
这种抽象层确保了即使底层实现不同,Elo 表达式的时间语义在所有平台上都是一致的。
工程实践:测试策略与工具链
Elo 项目采用严格的测试驱动开发(TDD)方法,确保编译器的正确性和跨平台一致性。
测试金字塔结构
- 单元测试:覆盖解析器、AST 构建、类型推断等基础组件
- 集成测试:验证整个编译流程,检查生成的代码语法正确性
- 验收测试:在实际的运行时环境中执行生成的代码,验证语义一致性
测试套件包含了数百个测试用例,每个测试用例都会在三个目标平台上运行,确保 “一次编写,到处运行” 的承诺得以实现。
CLI 工具设计
Elo 提供了两个命令行工具,分别面向不同的使用场景:
编译器(eloc):
# 编译表达式到 JavaScript(默认)
./bin/eloc -e "2 + 3 * 4"
# 编译到 Ruby
./bin/eloc -e "2 + 3 * 4" -t ruby
# 编译到 SQL
./bin/eloc -e "2 + 3 * 4" -t sql
# 包含前导代码
./bin/eloc -e "NOW + PT2H" -t ruby -p
求值器(elo):
# 直接求值表达式
./bin/elo -e "2 + 3 * 4"
# 输出: 14
# 使用输入数据
./bin/elo -e "_.x + _.y" -d '{"x": 1, "y": 2}'
# 输出: 3
程序化 API
对于需要在应用程序中集成 Elo 的开发者,项目提供了 TypeScript/JavaScript API:
import { compile } from '@enspirit/elo';
import { DateTime, Duration } from 'luxon';
// 编译表达式为可调用函数
const addTen = compile<(x: number) => number>(
'_ + 10',
{ runtime: { DateTime, Duration } }
);
addTen(5); // => 15
API 设计注重类型安全和运行时依赖注入,避免了全局变量污染,保持了代码的纯净性。
实际应用场景与最佳实践
1. No-Code 工具中的业务规则
在 Klaro Cards 等 No-Code 工具中,Elo 用于定义计算字段、过滤条件和汇总函数:
# 计算含税价格
_.price * 1.21
# 过滤高价值订单
filter(_, fn(o ~> o.total > 1000))
# 周汇总
_.amount when _.date in SOW ... EOW
2. 数据验证与转换
Elo 的模式验证功能非常适合数据清洗和验证场景:
let Product = {
id: String,
price: Float(p | p > 0),
category: String(c | c in ['electronics', 'clothing', 'food'])
} in input |> Product
3. 跨平台查询构建
对于需要在应用程序和数据库层使用相同逻辑的场景,Elo 提供了完美的解决方案:
# 业务逻辑:获取最近30天的活跃用户
let
cutoff = TODAY - P30D
in
filter(_, fn(u ~> u.last_login >= cutoff))
这个表达式可以同时用于:
- 前端:过滤 JavaScript 数组
- 后端:过滤 Ruby 集合
- 数据库:生成 SQL WHERE 子句
4. 测试断言语言
在 Webspicy 测试框架中,Elo 可以用作更强大的断言语言:
# 替代原来的简单断言
assert: _.status == 200 and _.body.items.size() > 0
技术挑战与解决方案
挑战 1:语义差异处理
不同编程语言在细节上存在诸多差异,例如:
- JavaScript 使用
&&、||、!,SQL 使用AND、OR、NOT - 幂运算:JavaScript 用
Math.pow(),Ruby 用**,SQL 用POWER() - 空值处理:JavaScript 的
nullvs SQL 的NULL
解决方案:在 IR 层使用统一的逻辑操作节点,在代码生成阶段根据目标语言进行转换。
挑战 2:时间处理一致性
不同平台的时间库 API 差异巨大,且时区处理复杂。
解决方案:定义抽象的时间操作接口,为每个平台提供适配器实现,统一时区处理逻辑。
挑战 3:性能优化
编译到 SQL 时需要生成高效的查询表达式。
解决方案:在 IR 层进行表达式优化,如常量折叠、公共子表达式消除,针对 SQL 生成器进行特殊优化。
未来发展方向
根据项目路线图,Elo 语言有几个重要的发展方向:
- 关系代数支持:集成 Bmg 关系代数库,支持更复杂的数据转换和查询操作
- 扩展标准库:增加更多数据处理函数,特别是针对数组和对象的操作
- 性能优化:改进编译器和生成代码的性能
- 更多目标平台:考虑支持 Python、Java 等其他流行语言
总结
Elo 语言代表了领域特定语言(DSL)设计的一个成功案例。它通过专注于数据表达式这一特定领域,在简单性、安全性和可移植性之间找到了良好的平衡点。多目标编译架构不仅解决了 No-Code 工具的实际需求,也为其他需要跨平台一致性的场景提供了参考方案。
对于开发者而言,Elo 的价值在于它提供了一种声明式的、类型安全的数据操作方式。对于最终用户而言,Elo 降低了编程门槛,让他们能够用直观的方式表达业务逻辑。随着 No-Code 运动的深入发展,像 Elo 这样的工具语言将在构建更智能、更易用的软件系统中发挥越来越重要的作用。
资料来源:
- Elo 语言官方网站:https://elo-lang.org/
- GitHub 仓库:https://github.com/enspirit/elo
通过统一类型系统和多阶段编译架构,Elo 证明了即使是看似简单的数据表达式语言,也能通过精心设计实现强大的跨平台能力。这为未来的 DSL 设计提供了宝贵经验:专注于解决特定问题,通过抽象和分层处理复杂性,最终实现简单而强大的用户体验。