# 语法与风格
Deft 的表层语法很小,只有少数几种互补的语法形式。本页介绍四种调用形态、
选择使用哪一种的规则,以及贯穿本文档其余部分的惯用风格。
## 三种调用形式
Deft 有三种函数调用的语法形式,另外还有用于分组中缀表达式的圆括号。
| 形式 | 示例 | 用途 |
|------|---------|-----|
| 前缀 `[...]` | `[trim $str]`, `[split $str ":"]` | 规范形式。适用于任意参数个数。在较大表达式中对调用分组时必须使用。 |
| 后缀 `$var(...)` | `$str(trim)`, `$items(first)`, `$xs(2..5)` | 变量上单参数调用的语法糖——等同于 `[trim $str]`。用于接收者风格的访问时读起来很自然。范围参数 `(a..b)` 执行切片而不是调用(见下文)。 |
| 裸形式(无括号) | `echo $name`, `println "hi"` | TCL 风格,解析结果与 `[echo $name]` 相同。允许出现在语句位置,这样函数调用就能与控制流关键字(`if`、`try`、`for`、`def`)自然并列,后者也是裸形式。 |
### 关键规则
裸圆括号 `(...)` 绝不是函数调用。它们只用于分组中缀
表达式:`($a * $b)`、`($n == 0)`。`(trim $str)` 这种形式是
语法错误。
# ✅ Preferred [trim $str] # prefix $str(trim) # postfix (same thing) echo "hello" $name # bare top-level statement ($price * $quantity) # infix expression # ❌ Invalid (trim $some-str) # parens are NOT function calls
## 何时使用哪种形式
### 裸形式——顶层语句
在语句位置使用裸形式,这样函数调用就能与控制流关键字自然并列。
任何一次性语句,去掉括号噪音后都更好读。
echo "Count: $xs(len)" println "result:" $result fs/write "out.txt" $content http/serve $router 8080
### 后缀形式——变量上的单参数接收者调用
对常见的接收者风格访问模式使用 `$var(call)`:对单个值的查询
与转换。
set length $name(len) # length of a string or collection set first $items(first) # first element set upper $name(upper) # upper-case a string set parts $str(split ",") # split a string set rounded $n(round) # round a number
### 索引与切片访问——`$x(a..b)`
后缀位置的范围参数会对字符串或集合执行切片,而不是调用函数——
`"hello"(1..3)` 得到 `"el"`。同样的语法可用于字符串和列表
字面量、变量以及 `~` 字段链。这里的 `..` 是范围运算符,不是
展开运算符。
set s "hello world" $s(0..5) # => "hello" end is exclusive: indexes 0..4 $s(2..) # => "llo world" open end → through the last element $s(..5) # => "hello" open start → from the first $s(-3..) # => "rld" negative bounds count from the end def xs @{ 10 20 30 40 50 } $xs(1..3) # => @{ 20 30 } $xs(..2) # => @{ 10 20 } $xs(3..) # => @{ 40 50 } $xs(0..-1) # => @{ 10 20 30 40 } (all but the last) $xs(2) # => 30 plain index — equivalent to nth "string"(0..1) # => "s"
结束端是排他的:`(0..n)` 包含索引 `0` 到 `n-1`。
负边界从字符串或集合的末尾往前数。
开放端范围(`a..` 和 `..b`)不需要占位数字。
后缀形式也可以通过 `~` 链式进行字段访问:
set role $user~profile~role # nested field access set name-len $user~name(len) # postfix on a field
### 前缀形式 `[...]`——其他情况
在以下情况使用 `[...]`:
- 调用是较大表达式的一部分(赋值、参数、条件)。
- 调用有多个参数,不符合接收者模式。
- 函数使用斜杠命名空间(`fs/read`、`http/get`、`sqlite/query`)——
后缀形式可用于变量,但斜杠名称的接收者调用很别扭。
set contents [unwrap [fs/read "config.txt"]] set rows [sqlite/query $db "SELECT * FROM users"] set result [math/sqrt (($x * $x) + ($y * $y))] set found [find $xs { ($it > 10) }]
### `(...)` 内的中缀——算术与比较
算术(`+ - * / mod`)、比较(`== != < > <= >=`)、布尔
(`and or`)和位运算都位于圆括号内:
set total ($price * $quantity) set next ($i + 1) if ($n == 0) { return 1 } if ($age >= 18 and !$minor?) { ... } set masked ($flags & 0xff)
运算符只能在 `(...)` 内以中缀形式使用——它们不是一等
函数,不能用 `[...]` 调用。`[+ $a $b]`、`[== $n 0]`、
`[and $x $y]`、`[or $x $y]` 全都无效:它们恰好能对正好两个操作数
通过编译(解析器的一个怪癖),但其他情况——`[+ 1 2 3]`、
`[or a b c]`——会在运行时失败,这与 Lisp 程序员的预期恰好
相反。始终使用中缀形式。
### 取反——一元 `!`
`!` 是一元运算符,可用于任何表达式位置,与一元 `-` 对应。
它绑定得很紧(和 `-` 一样),所以 `(!$a == $b)` 等同于
`(!$a) == $b`;要对整个比较取反,把它分组:`(!($a == $b))`——
或者直接使用 `!=`。在语法本身已接受表达式的地方,单独取反外的
圆括号是可选的:
if !$flag { set msg "off" } if ![nil? $m] { return } if ($a and !$b) { ... } set y (!!$x) # double bang set y [not $x] # the word form is also a call (one arg)
旧的括号前缀写法 `(not x)` 是解析错误——诊断信息会指向
`!`。`not` 关键字保留在两处:
调用形式 `[not x]` 和否定匹配模式(`match` 分支中的
`not :admin`)。
> `[...]` 只能是前缀;`(...)` 只能是中缀。两者不能混用:
> `(* $a $b)`、`[+ $a $b]`、`[== $n 0]` 和 `(trim $str)` 都是
> 语法错误。
## 展开运算符
`..$xs` 会把列表元素就地拼接——拼接进函数调用的参数或
向量字面量。`..` 前缀是展开(spread)标记;其后的表达式必须
求值为列表。
def f {|a b c| ($a + $b + $c)} def xs @{1 2 3} [f ..$xs] # => 6 — passes 1, 2, 3 as three args [f 0 ..@{1 2}] # => 3 — fixed args mix freely with spreads [f ..@{1 2} 3] # => 6 — the spread expression needn't be a variable set ys @{0 ..$xs 4} # vector literal: @{0 1 2 3 4}
说明:
- 展开是运行时拼接——列表在调用执行时被展平,而不是在
代码解析时。不要把它与宏引用中使用的编译期 `[macro/splice $list]`
搞混(见 宏)。
- 展开参数必须是列表;其他任何东西都会引发运行时
错误(`spread argument must be a list`)。
- 展开只在前缀调用和 `@{...}` 向量字面量中被接受——后缀调用
(`$f(...)`)和 `%{...}` 映射字面量不支持它。
- 在列表模式中,`..$rest`(或 `| $rest`)收集
匹配列表的尾部——见 语法。
## 管道
`|>` 运算符让值穿过一串单参数转换。每一步把上一步的
结果作为隐式的最后一个参数接收。
# Multi-step collection transform @{ 1 2 3 4 5 } |> filter { ($it % 2) == 0 } |> map { $it * 2 } |> collect # => @{ 4 8 } # String processing "$input" |> trim |> lower |> (split ",") |> map trim |> join "|"
对于多步转换,优先使用管道,而不是嵌套调用或中间 `def`
绑定。只有当调用是较大表达式的一部分时,才动用 `[...]`。
若要以转换器(transducer)风格复用(把同一管道应用于多个
集合或汇聚点),见 转换器。
## 状态与赋值
`set` 重新绑定局部变量;`def` 在模块作用域引入一个变量。完整的
作用域规则见 Set 与 Def。
def base-url "https://api.example.com" # module-scoped def counter 0 def bump {|amount^number| set counter ($counter + $amount) # rebind within the def $counter }
`<<` 就地修改集合类型的局部变量并返回修改后的集合——
适合累积操作:
def out @{} for x in $xs { if ($x > 0) { $out << $x } # mutates $out, returns it }
数值自增优先使用 `incr`:
incr i # preferred set i ($i + 1) # works, but unidiomatic
## 字符串插值
双引号字符串和反引号字符串支持五种插值形式:
| 形式 | 用途 | 示例 |
|------|-----|---------|
| `$name` | 简单变量 | `"hello $name"` |
| `$name~field` | 带字段链的变量 | `"$user~name"` |
| `$name(args)` | 后缀调用 | `"count: $xs(len)"` |
| `$(expr)` | 中缀表达式 | `"total: $($qty * $price)"` |
| `$[func args]` | 前缀函数调用 | `"count: $[len $xs]"` |
中缀形式与字符串之外的中缀分组语法一致——`(` 和 `)`
在任何地方都用于分组算术和比较。`$(...)` 内的裸标识符
也可以作为变量查找使用(例如 `$(name)`)。
用 `\$` 转义以得到字面量 `$`。字面量 `[`、`]`、`{`、`}`、`(`、`)` 在
双引号字符串内无需转义。
def name "Alice" set age 30 set role :admin set msg "Hello $name, age $($age + 1), role: $[upper $role]" # => "Hello Alice, age 31, role: ADMIN" set hint "Press \[Enter] to continue (\$5 fee)"
## 命名约定
### 谓词——`X?` 后缀
谓词使用 `X?` 后缀(Clojure 风格),绝不使用 `is-X` 前缀。
这适用于标准库函数(`nil?`、`empty?`、`even?`、`starts-with?`、
`ends-with?`、`chars/word?`、`chars/digit?`、`chars/alpha?`)以及
用户定义的谓词(`attached?`、`image-ext?`、`fresh?`)。
def admin? {|user| :admin in? $user~roles} # ✓ predicate def is-admin {|user| ...} # ✗ avoid
### 数学函数——按使用频率划分,而非按领域
数值标准库函数分为两类;划分边界是使用频率,而不是“领域”:
- 裸形式(无命名空间前缀):日常代码中出现的那一小批通用运算——
`abs`、`floor`、`ceil`、`round`、`min`、`max`、`clamp`、`random`、
`mod`、`int`。
- `math/` 命名空间:专用 / 很少使用的运算——`math/sqrt`、
`math/pow`、`math/sum`、`math/avg`、`math/sin`、`math/cos`、
`math/lerp`、`math/random-range`、`math/pi` 等。
新的数值标准库函数如果会被经常使用,就采用裸形式(`median`
大概就值得裸用 `median`),否则放在 `math/` 下。这种划分与 Python
的内置函数与 `math` 模块之分相呼应,也让顶层命名空间保持整洁。
### 标识符使用 kebab-case
变量名和函数名使用 `kebab-case`(`my-cool-function`、
`user-name`)。类型名使用 `PascalCase`(`%Point`、`%User`)。
关键字使用 `kebab-case`(`:on-init`、`:row-count`)。
## 注释
`#` 开始一个行注释,直到行尾。`#|` 开启一个块注释,直到
匹配的 `|#` 结束;它可以跨任意多行,内容是原始文本——每行
不需要 `#` 前缀。块注释可以嵌套:`#| ... |#` 内的 `#|`
会增加深度,只有数量匹配的 `|#` 结束符才能结束注释。
# Compute the Euclidean distance from origin. def distance {|p^Point| [math/sqrt (($p~x * $p~x) + ($p~y * $p~y))] } #| Block comments span lines with no per-line #: handy for long explanations, license headers, or temporarily disabling a chunk of code. They nest too: #| inner #| deepest |# back out |# |#
def distance {|p^Point| @doc "Euclidean distance from origin." [math/sqrt (($p~x * $p~x) + ($p~y * $p~y))] }
## 向集合追加元素
当接收者是函数局部变量时,`<<` 会就地修改它。累积时使用
裸形式 `$xs << x`——它返回修改后的集合,因此既能作为语句
使用,也能用于表达式上下文。
# Preferred — mutates $xs in place, returns the (mutated) collection $xs << 4 # Equivalent, slightly more verbose — still works, still mutates set xs ($xs << 4) # Pure-functional — does NOT mutate, returns a new collection. Use when you # need to preserve the original. [append $xs 4]
要在一条语句中追加多个值,请使用分开的 `$xs << a`、
`$xs << b` 等语句。链式中缀 `$xs << a << b` 目前不会
沿链传播该局部变量。
## 从闭包中修改外层局部变量
从闭包内部(`{ … }`、`each`/`map`/`filter` 的函数体、`defer` 等)
修改局部变量,会通过堆装箱的按引用捕获传回外层作用域。`<<`、
`set` 和 `incr` 都能透明地跨过这一边界。
# Accumulates into $out — bare `<<` mutates the captured local in place @{ 1 2 3 } |> each { $out << $it } # Also works inside plain `for` loops (the body is inlined into the same frame) for x in $xs { $acc << $x }
性能说明: 被任何内层闭包捕获的局部变量都会被装箱
(每个被捕获的槽位一个 `ObjBox`,在函数入口处分配一次)。不涉及
闭包的热点数值循环仍然使用快速的 `load_local`/`store_local`
操作码,零开销。如果你需要在紧凑循环中获得最大吞吐量,请把循环体
提取到独立的 `def` 中,这样循环变量就不会被任何东西捕获。
## 风格速查表
# ✅ Preferred set total ($price * $quantity) # infix expression set length $name(len) # postfix on a variable $items |> filter even? |> map { $it * 2 } |> collect # pipeline echo "hello" $name # bare top-level incr i # numeric increment # ❌ Avoid set length [len $name] # use postfix $name(len) set i ($i + 1) # use incr i for unit increments # ❌ Invalid (trim $some-str) # parens are NOT function calls [+ $a $b] # operators are infix-only, not prefix calls [== $n 0] # use ($n == 0) [or $a $b] # use ($a or $b)
## 单行 `if`
对于简单的条件和表达式,单行 `if` 形式最易读:
set msg [if logged-in? "Welcome!" "Please log in."] if ($n == 0) { return 1 } if ($count > 10) { set msg "too many" }
对于复杂逻辑,使用带 `else` 的多行形式:
if logged-in? { "Welcome!" } else { if admin? { "Welcome, admin!" } else { "Welcome, user!" } }
条件、循环和清理形式的完整集合见 控制流。