# 语法与风格 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 |# |#
未终止的块注释(`#|` 没有闭合的 `|#`)是词法错误,不会被 静默接受。 为函数编写文档时,优先在函数体内使用 `@doc`(见 函数 和 元编程):
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!"
    }
}
条件、循环和清理形式的完整集合见 控制流。