Skip to content

RFC 012: F-String 模板字符串

摘要

为 YaoXiang 语言添加 f-string 模板字符串特性,支持变量插值、表达式求值和格式化输出。f-string 使用 Python 风格语法(f"..." 前缀),在字符串中通过 {expression} 语法嵌入表达式,编译时转换为高效的字符串操作。

注意: f-string 语法和行为与 Python 保持一致,具体规范参考 Python 官方文档

动机

为什么需要这个特性?

当前 YaoXiang 的字符串拼接方式较为繁琐:

yaoxiang
# 现状:使用 + 拼接
name = "Alice"
age = 30
message = "Hello ".concat(name).concat(", age: ").concat(age.to_string())
print(message)

# 或者使用 format 函数
message2 = format("Hello {}, age: {}", name, age)

当前的问题

  1. 可读性差:字符串拼接和格式化需要多处调用,代码冗长
  2. 易出错:手动处理类型转换,容易遗漏 .to_string()
  3. 性能考虑:多次字符串拼接可能影响性能
  4. 表达力不足:无法直观地在字符串中嵌入复杂表达式

提案

核心设计

引入 f-string 作为新的字符串字面量前缀,支持:

  • 变量插值f"Hello {name}"
  • 表达式求值f"Sum: {x + y}"
  • 格式化说明符f"Pi: {pi:.2f}"
  • 类型安全:编译时检查表达式类型

示例

yaoxiang
# 基础插值
name = "Alice"
greeting = f"Hello {name}"  # "Hello Alice"

# 表达式插值
x = 10
y = 20
result = f"Sum: {x + y}"    # "Sum: 30"

# 格式化说明符
pi = 3.14159
formatted = f"Pi: {pi:.2f}"  # "Pi: 3.14"

# 复杂表达式
items = [1, 2, 3]
s = f"Count: {len(items)}, sum: {sum(items)}"  # "Count: 3, sum: 6"

# 对象方法调用
user = User("Bob", 25)
bio = f"Name: {user.name}, age: {user.get_age()}"

语法变化

之前之后
"Hello ".concat(name)f"Hello {name}"
format("Value: {}", value)f"Value: {value}"
format("Pi: {:.2f}", pi)f"Pi: {pi:.2f}"

语法规范

FStringLiteral ::= 'f' '"' FStringContent* '"'
FStringContent ::= FStringChar | EscapeSequence | FStringInterpolation
FStringInterpolation ::= '{' Expression (':' FormatSpec)? '}'
FormatSpec      ::= [width] ['.' precision] type
width           ::= digit+
precision       ::= digit+
type            ::= 'b' | 'c' | 'd' | 'e' | 'E' | 'f' | 'F' | 'g' | 'G' | 'n' | 'o' | 's' | 'x' | 'X' | '%'

详细设计

语法分析

编译器在词法分析阶段识别 f 前缀字符串字面量,解析花括号内的表达式和可选的格式化说明符。

转换策略

f-string 在编译时转换为高效的字符串操作:

简单插值

yaoxiang
f"Hello {name}"

转换为:

yaoxiang
"Hello ".concat(name.to_string())

表达式插值

yaoxiang
f"Sum: {x + y}"

转换为:

yaoxiang
"Sum: ".concat((x + y).to_string())

格式化说明符

yaoxiang
f"Pi: {pi:.2f}"

转换为:

yaoxiang
format("Pi: {:.2f}", pi)

多个插值

yaoxiang
f"Hello {name}, you are {age} years old"

转换为:

yaoxiang
"Hello ".concat(name.to_string()).concat(", you are ").concat(age.to_string()).concat(" years old")

类型系统影响

  • 插值表达式必须实现 Stringable 接口(自动为基本类型和字符串实现)
  • 格式化说明符要求类型支持相应格式化
  • 编译器检查表达式类型和格式化规则的匹配

编译器改动

组件改动
lexer识别 f 前缀,解析字符串内插值语法
parser新增 FStringLiteral 语法节点
typecheck检查插值表达式类型,验证格式化规则
codegen生成字符串拼接或格式化调用代码

向后兼容性

  • ✅ 完全向后兼容
  • 现有字符串字面量 "..." 保持不变
  • f-string 是新增语法,不影响现有代码

权衡

优点

  1. 语法简洁:减少样板代码,提高可读性
  2. 类型安全:编译时检查,减少运行时错误
  3. 性能优化:编译器可优化字符串拼接
  4. 表达力强:支持任意表达式和格式化
  5. 学习成本低:与 Python 生态一致

缺点

  1. 编译器复杂度:需要新增语法分析和转换逻辑
  2. 语法歧义:需要与现有字符串语法区分
  3. 调试挑战:编译后代码与源代码结构不同

替代方案

方案为什么不选择
仅支持变量插值无法满足复杂格式化需求
使用函数式风格 format(...)语法不够简洁
延迟到 v2.0用户对字符串便利性有明确需求
使用反引号或其他前缀与 Python 生态不一致

实现策略

阶段划分

  1. 阶段 1 (v0.9):

    • 基础 f-string 语法支持
    • 变量和简单表达式插值
    • 基础类型转换
  2. 阶段 2 (v1.0):

    • 格式化说明符支持
    • 复杂表达式插值
    • 性能优化
  3. 阶段 3 (v1.1):

    • 调试信息增强
    • 错误信息改进
    • 更多格式化选项

依赖关系

  • 无外部依赖
  • 需要基础类型系统支持
  • 需要字符串库基础功能

风险

  1. 性能风险:多个插值可能导致过多字符串对象
    • 缓解:编译器优化相邻字符串常量合并
  2. 类型检查复杂性:格式化说明符的类型检查
    • 缓解:参考 Python 实现,使用简单直接的检查
  3. 语法歧义{} 的嵌套使用
    • 缓解:明确语法规则,限制嵌套

开放问题

  • [x] 是否支持转义的大括号?与 Python 一致:使用双大括号表示单大括号,如 {{ 表示 {}} 表示 }
  • [x] 是否支持自定义格式化函数?与 Python 一致:支持通过 __format__ 方法自定义类型的格式化行为
  • [x] 格式化说明符的完整规范?与 Python 一致,详见上文 BNF
  • [x] 性能优化的具体策略?与 Python 一致:运行时拼接,无需特殊优化
  • [x] 错误诊断的最佳实践?与 Python 一致:报错时显示原 f-string 内容和位置

附录

附录A:格式化说明符参考

类型说明符示例输出
整数df"{42:d}""42"
浮点ff"{3.14:.2f}""3.14"
科学计数ef"{1000:e}""1.000000e+03"
字符串sf"{name:s}""Alice"
十六进制xf"{255:x}""ff"

附录B:使用场景示例

yaoxiang
# 日志记录
log(level: String, msg: String, count: Int) = () => {
    timestamp = get_timestamp()
    print(f"[{timestamp}] {level}: {msg} (count: {count})")
}

# JSON 构建
json = "{\n    \"name\": \"".concat(user.name).concat("\",\n    \"age\": ")
    .concat(user.age.to_string()).concat(",\n    \"email\": \"")
    .concat(user.email).concat("\"\n}")

# SQL 查询构建(注意 SQL 注入风险)
query = f"SELECT * FROM users WHERE age > {min_age} AND status = '{status}'"

# 调试信息
debug_info = f"Point({x:.2f}, {y:.2f}) at {timestamp}"

# 条件格式化
status_msg = if is_active {
    f"User {name} is active"
} else {
    f"User {name} is inactive"
}

参考文献


生命周期与归宿

RFC 有以下状态流转:

┌─────────────┐
│   草案      │  ← 作者创建
└──────┬──────┘


┌─────────────┐
│  审核中     │  ← 社区讨论
└──────┬──────┘

       ├──────────────────┐
       ▼                  ▼
┌─────────────┐    ┌─────────────┐
│  已接受     │    │  已拒绝     │
└──────┬──────┘    └──────┬──────┘
       │                  │
       ▼                  ▼
┌─────────────┐    ┌─────────────┐
│   accepted/ │    │    rfc/     │
│ (正式设计)  │    │ (保留原位)  │
└─────────────┘    └─────────────┘