语言特性
Emo 语法简洁、显式定义、符合直觉。它汲取三十年开源编程语言的经验与教训 —— 以下是让这门语言成为它自己的那些决定。
语法:一切所见即所得
Emo 的语法偏爱明确:调用看起来像调用,返回一定写出来,代码块只有一种形状。
- 调用永远带括号,且紧贴被调者。没有可选括号调用;
f (a)中间有空格就是语法错误,而不是被猜成一次调用。 - 调用可以尾随一个代码块作为最后一个实参。
page(title: "Home") { ... }—— 这是 UI 树与回调背后的唯一记法。 - 代码块只有一种形状:
{ ... }与-> (x) { ... },后者兼作匿名函数。 - 绑定是
const(不可变)或var(可变、块作用域)。var不能逃逸出它的块,被存活期更长的闭包捕获是编译错误。 - 实参可以按位置或按名传递。命名实参是 Props 与选项的自然形态。
- 类型标注后置,以空格分隔;函数签名永远显式标注参数与返回值 —— 签名即契约,契约必严查。
- 谓词方法以
?结尾:user.adult?()在调用点读起来就像一句话。 return永远显式;if只有一种形状,没有else if;case按模式匹配,可带守卫。- 命名大小写约定由编译器强制:类型大写开头,其余一律小写 snake_case。
- 字符串永远双引号,插值只有一种形式:
"你好, ${name}"。
类:唯一的用户自定义类型
class 是定义类型的唯一记法,方法就住在类体里,紧挨着它所属的类型。
class User {
def init(name String, age Int64) {
self.name = name
self.age = age
}
def full_name() String {
return self.name + " " + self.age.to_string()
}
def is_older?() Bool {
return self.age > 35
}
}init是字段唯一能被赋值的窗口。类因而是不可变值类型:赋值即拷贝(底层写时复制),内容相等则==。self隐式传入、显式使用 —— 字段与局部变量永不混淆。- 没有继承,单继承多继承都没有。复用交给鸭子类型、组合与接口。
emo 块:函数组的命名空间
emo 块把一组函数与 const 常量组织在一起,充当某种程度上的命名空间 —— Math.abs(-5) 以块名直接调用,无需创建任何实例。
// emo 块: 把一组函数和常量组织成命名空间
emo Math {
const phi = 1.618
def abs(n Int64) Int64 {
if n < 0 {
return -n
}
return n
}
def square(n Int64) Int64 {
return n * n
}
}
println(Math.abs(-5)) // 5
println(Math.square(6)) // 36
println(Math.phi) // 1.618- 是函数组,不是类型。块内是普通函数与
const常量,没有self,也不创建实例。 - 块名即命名空间。函数以块名直接调用,常量直接以
Math.phi取用。 - 相关函数住在一起。比裸函数多了归属,比类少了仪式,使用处自带上下文。
接口:结构化多态
interface 声明一组方法签名,任何形状匹配的类都满足它 —— 没有 implements。 接口属于消费方;检查在编译期,分派在运行时保持鸭子类型,零开销。 收窄一视同仁:g.is(Admin) 为真后,g 的类型自动变窄。
// 形状匹配即满足 —— 没有 implements, 接口属于消费方
interface Shape {
def area() Float64
}
class Rect {
def init(w Float64, h Float64) {
self.w = w
self.h = h
}
def area() Float64 {
return self.w * self.h
}
}
class Circle {
def init(r Float64) {
self.r = r
}
def area() Float64 {
return 3.14159 * self.r * self.r
}
}
def describe(s Shape) String {
if s.is(Circle) {
return "一个圆, 面积 ${s.area()}"
}
return "一个矩形, 面积 ${s.area()}"
}
println(describe(Rect.new(w: 2.5, h: 4))) // 一个矩形, 面积 10.0
println(describe(Circle.new(r: 1))) // 一个圆, 面积 3.14159枚举与错误
枚举是封闭的、有名值集合 —— 仅此而已,成员不携带数据(这是刻意排除的反模式)。 带数据的场景用「枚举标签 + 元组」的惯用法:(Outcome.ok, value),在 case 与 receive 里直接按位置解构。
错误就是异常,异常就是普通的类实例:raise Exception.new(message: "...")。 未捕获的异常只杀死出错的进程;异常不进函数签名,编译器也不强制处理。
// 枚举: 封闭的有名值集合 —— 成员不带数据
enum Outcome {
ok
error
}
// 带数据的场景: 「枚举标签 + 元组」的惯用法
const ok = (Outcome.ok, 42)
const err = (Outcome.error, "not found")
def show(result) {
case result {
(Outcome.ok, value) -> { println("成功: ${value}") }
(Outcome.error, why) -> { println("失败: ${why}") }
}
}
show(ok) // 成功: 42
show(err) // 失败: not found
// 错误就是异常: 普通的类实例, raise 抛出
def divide(a Float64, b Float64) Float64 {
if b == 0 {
raise Exception.new(message: "除数为零")
}
return a / b
}
println(divide(10, 4)) // 2.5可变性:分层的,且每层都显式
const绑定永不改变;var在块内可变、不能逃逸。- 数组是不可变值:变换返回新数组,
==逐元素比较。 - 类字段只在
init内赋值,之后冻结。 - 长期可变状态住在
Box里:Box.new(0)构造,box.read()读,box.replace(v)换 —— 刻意没有别的操作。Box 发往另一个进程时是快照拷贝,可变性永不跨越进程边界。
// const 永不改变; var 在块内可变
const base = 10
def bump() Int64 {
var count = base
count = count + 1 // var: 块内可变
return count // 逃出去的是值, 不是可变性
}
// 数组是不可变值: == 逐元素比较
const xs = [1, 2, 3]
const ys = [1, 2, 3]
println(xs == ys) // true
// 类字段只在 init 内赋值, 之后冻结
class Point {
def init(x Float64, y Float64) {
self.x = x
self.y = y
}
}
const p = Point.new(x: 1, y: 2)
// 长期可变状态住在 Box 里: 构造、读、换
const box = Box.new(0)
println(box.read()) // 0
box.replace(box.read() + 1)
println(box.read()) // 1
println(bump()) // 11// 反例: 闭包活得比 var 长 —— 编译错误
def make_counter() {
var count = 0
return -> { count = count + 1 } // 编译错误: var 不能逃逸它的块
}
// 正解: 长命可变状态, 显式住进 Box
def make_counter() {
const box = Box.new(0)
return -> { box.replace(box.read() + 1) } // box 是 const,可以逃逸
}
const next = make_counter()
println(next()) // 1
println(next()) // 2类型系统:渐进式
运行时动态类型,编译期静态检查。
- 运行时每个值带类型标签 —— 与 BEAM 天然对齐,日常代码没有类型仪式。
- 编译器内置类型检查:标注可选(函数签名除外),未标注的代码照样推断与检查,只报告确定的错误。
- 结构化、流敏感:
user.is(Admin)为真后,user收窄为Admin。 - 没有泛型机器:参数化类型只是标注词汇(
Array[User]、Box[Int64]),应用代码几乎见不到类型拼写。 - 类型信息反哺性能:类型足够完整的模块在原生后端被特化 —— 去装箱表示、直接分派。
// 函数签名必写类型 —— 签名即契约
def add(a Int64, b Int64) Int64 {
return a + b
}
// 其余标注可选, 编译器照样推断与检查
const total = add(19, 23) // Int64
const xs = [1, 2, 3] // Array[Int64]
// 想写得更具体时, 参数化类型只是标注词汇
const bigs Array[Int64] = [100, 200]
// 流敏感: is() 为真后, 类型自动收窄
class User {
def init(name String) { self.name = name }
}
class Admin {
def init(name String) { self.name = name }
def title() String { return "管理员 ${self.name}" }
}
def label(u User) String {
if u.is(Admin) {
return u.title() // u 已收窄为 Admin
}
return "用户 ${u.name}"
}
println(label(User.new(name: "小明"))) // 用户 小明
println(label(Admin.new(name: "大刘"))) // 管理员 大刘模块:目录树即模块树
完全结构化的模块系统:没有 import,没有 export,没有可见性关键字。
shop/
order.emo // 模块 shop.order
pricing.emo // 模块 shop.pricing
internal/
discounts.emo // 模块 shop.internal.discounts —— 子树私有
checkout.emo // 模块 shop.checkout// 没有 import: 路径即引用, 别名就是一个普通的 const
const order = shop.order
def checkout(cart Cart) Float64 {
const total = order.total(cart)
return total
}- 路径即模块。文件自动成为其路径上的模块,无需注册与声明。
- 引用即限定路径。像 URL 一样直接使用;路径太长时,一个普通的
const就是别名。 - 可见性是结构性的。
internal/目录子树私有,由编译器强制。 - 依赖图是显式的。路径引用就是依赖声明,自动获得模块发现、增量编译与编译期循环检测。
包管理:构建即安装
包直接嵌入模块系统:包名成为顶层模块路径前缀。库没有 install 步骤 —— 依赖解析是构建的副产品。 使用一个包就是 require "acme/json_tools";manifest 里缺失的 require 是编译错误。
package {
name = "acme/json_tools"
version = "0.1.0"
targets = ["native", "wasm"]
deps {
json = "2.3.1"
http = "1.4.2"
}
}- 中心注册表,端点可配(
EMO_REGISTRY),支持私有与内网分发;全局内容寻址缓存,项目间共享。 - 作用域包名,杜绝抢注;官方标准库独占顶层短名(
json.decode()、http.get(url))。 - 语义化版本 + MVS 解析,升级永远是显式动作;
package.lock记录校验和,纳入版本控制,构建永不静默改写。 - 目标兼容性在解析期检查:不支持当前目标的依赖直接报清晰的错。
并发:进程与消息传递
Emo 内建围绕进程与消息传递的原生并发模型,Actor 模型的嫡系 —— 直接映射 BEAM 进程,原生后端用 OCaml 5 effects 实现调度器。
do启动进程并返回 pid;pid <- message发送;receive复用case的分支语法,选择性接收来自普通模式。- 惯用回复模式只有一行:
sender <- (self_pid(), request),元组在接收分支里直接解构。 halt()或未处理的错误只杀死出错的进程;kill、wait、重启策略是库的领土。- 数据默认不可变,消息在 BEAM 上拷贝、在原生后端传引用,可观察语义完全一致。
- 尾调用有保证;递归就是收信循环的惯用形态。
// do 起进程, <- 发消息, receive 收信箱
def worker() {
receive {
(from, n) -> {
from <- ("done", n * n) // 惯用回复: 收信人随消息一起传进来
worker() // 尾调用: 递归就是收信循环
}
}
}
const pid = do worker()
pid <- (self_pid(), 9)
receive {
(tag, result) -> { println("${tag}: ${result}") } // done: 81
}网络:一等公民,直接风格
统一的异步网络 API,由每个后端的原生设施实现:io_uring(Linux)、kqueue(macOS)、IOCP(Windows), BEAM 上一连接一进程,Wasm 上用 WASI sockets 或 fetch/WebSocket。
- 直接风格:网络调用看起来就是普通阻塞调用,调度器在底下切换进程。没有
async/await,因而没有函数染色。 - 核心库提供
net(TCP/UDP/Unix domain + TLS),标准库提供http(客户端与服务端)。 - TLS 证书校验默认开启;不安全变体是显式的、看起来就危险的
net.tls_connect_insecure。 - 超时以秒计;每种失败都抛出消息里写明对端、操作与原因的普通异常。
// 网络调用看起来就是普通调用 —— 没有 async, 没有函数染色
def fetch_body(url String) String {
const resp = http.get(url) // 看起来会阻塞, 调度器在底下切换进程
if resp.status() != 200 {
raise Exception.new(message: "GET ${url} 返回 ${resp.status()}")
}
return resp.body()
}
println(fetch_body("https://example.com"))原生构建:一条命令,一个可执行文件
built app
- 单一二进制即可完整运行。起进程、跑 HTTP 服务的程序,编译后行为一致,无需解释器、无需下载运行时。
- 编译输出对齐解释器标准。每个示例的二进制输出与
emo run逐字节一致 —— CI 断言,不是假设。 - 类型反哺性能。类型完全已知的函数编译成特化原生代码;
benchmarks/记录着数字。 - 增量构建。按内容哈希缓存,未变更的重建直接报告
(cached)。
// 声明 C 符号, 由生成的包装器完成编组
foreign def sqrt(x Float64) Float64 = "sqrt"
// 链接额外的 C 库:
// emo build main.emo --cclib m配置:Emo 就是自己的配置语言
配置文件就是一个 Emo 表达式,加载即求值 —— 不需要学第二种格式。 求值运行在受限 Profile 下:保证终止、结果密封、无副作用。 类型检查器内置,配置 Schema 就是类型标注,由同一趟检查完成。 对外交换仍支持 JSON / YAML / TOML:Emo 是事实之源,其他格式是导出产物。