# 函数
函数是 Deft 中最主要的抽象单元。本页介绍 `def` 形式、参数语法、
类型标注、闭包,以及 `@doc`/元标签装饰器。
## 定义函数
使用 `def`,函数体用花括号包围。参数在函数体开头的 `|...|` 内声明。
零参数使用 `||`。
def greet {|name^string| @doc "Print a greeting to stdout." echo "Hello, $name!" } def magic-number {|| 42 } greet "world" # => Hello, world! echo [magic-number] # => 42
函数体是一条或多条语句;最后一条语句的值即返回值。存在隐式 `return` ——
只在提前退出时使用显式 `return`:
def fib {|n^number| if ($n < 2) { return $n } [$fib ($n - 1)] + [$fib ($n - 2)] } echo [fib 10] # => 55
## 参数语法
参数位于 `|...|` 内。它们用逗号或空白分隔;两种方式都行。
def add {|a b| ($a + $b) } def add-also {|a, b| ($a + $b) } def variadic {|first @rest| $rest } # variadic via @
### 可变参数函数
`@rest`(或任意 `@name`)将剩余参数收集到列表中。`@`
与 `@{...}` 向量字面量对应 —— 它表示“收集列表的参数”。
必须放在最后;`@` 之后的名称按原样绑定:
def sum-all {|@nums| @doc "Sum any number of arguments." reduce $nums 0 { ($acc + $it) } } echo [sum-all 1 2 3 4] # => 10
### 默认值
参数可以用 `=value` 指定默认值:
def greet {|name="World"| echo "Hello, $name!" } greet # => Hello, World! greet "Alice" # => Hello, Alice!
## 类型标注
类型标注跟在参数名之后,用插入符(`^`)分隔。它们有三个用途:
1. 文档 —— 向读者传达期望的类型。
2. 校验 —— 在调用入口进行运行时类型检查。
3. 编辑器支持 —— 驱动自动补全和签名帮助。
def distance {|p^Point| [math/sqrt (($p~x * $p~x) + ($p~y * $p~y))] } def greet {|name^string count^number| for i [range $count] { echo "Hello, $name!" } }
类型标注并不强制静态类型 —— Deft 仍然是动态类型的。它们是“提示”,
用于改进工具支持,并在特定边界(函数入口、带类型的 `set`)触发校验。
### 基本类型
| 标注 | 描述 | 示例 |
|---|---|---|
| `string` | 文本 | `"hi"`, `'world'` |
| `number` | 数值 | `42`, `3.14` |
| `boolean` | 布尔值 | `true`, `false` |
| `bool` | boolean 的别名 | `true`, `false` |
| `nil` | 空值 | `nil` |
| `list` | 列表/数组 | `@{ 1 2 3 }` |
| `map` | 键值映射 | `%{ a 1 b 2 }` |
| `set` | 集合 | `^{ 1 2 3 }` |
| `keyword` | 关键字 | `:foo` |
### 用户定义类型
标注可以引用任何 `deftype`:
deftype User %{ :name %{:type :string :required true} :email %{:type :string} :age %{:type :number} } def display {|u^User| echo "$u~name <$u~email>" } def alice %User{ :name "Alice" :email "alice@example.com" :age 30 } display $alice
完整的 `deftype` 模式(schema)参考见 类型与协议。
### 运行时校验
标注会在调用入口触发校验:
def greet {|name^string| echo "Hello, $name!" } greet "Alice" # ✓ greet 42 # ✗ Error: type error in 'greet': parameter 'name' expected string, got number
未标注类型的参数接受任意值,因此可以自由混用:
def flexible {|typed^string untyped| # $typed is validated at call time # $untyped can be anything }
### 带类型的 `set`
`set` 可以携带类型标注来校验值:
set name^string "Alice" set count^number 42 set user^User [create-user "Bob"]
### 返回值类型标注
可以标注在函数名上……
def fetch-user^string {|id^string| # ... } }
## 命名可选参数(`--flag` 关键字参数)
当参数带有 `deftype` 标注(任何非基本类型的 `^TypeName`)时,
Deft 会将其视为标志收集参数。调用方传入 `--flag value`
键值对,而不是按位置绑定该参数;它前面的基本类型参数仍按位置传递。
这是表达可选命名参数的惯用方式 —— 参数类型的模式就是契约。
deftype BuildOpts %{ :push %{:type :boolean :default false} :nocache %{:type :boolean :default false} :tag %{:type :string} } def build {|service^string opts^BuildOpts| echo "building $service" if $opts~push { echo " will push" } if $opts~nocache { echo " no cache" } if ($opts~?tag) { echo " tag: $opts~tag" } } build "myapp" --push --tag latest # => building myapp / will push / tag: latest build "myapp" --nocache # => building myapp / no cache build "myapp" # => building myapp (opts omitted entirely)
### 工作原理
- 标注为用户定义类型的参数会将该位置变为标志收集参数。
它必须是最后一个参数 —— 它之前的所有参数都按位置传递且必需;
它之后不能再有参数。
- 在调用处,位置参数前缀之后的内容会被解析为 `--flag value`
键值对,并使用与关键字参数类型构造器相同的规则组装成
`%TypeName{...}` 实例(见类型与协议)。
- 布尔字段接受裸 `--flag` 简写,表示 `true`
(`--push` 等价于 `--push true`)。
- 值会根据模式中的 `:type` 进行类型检查;不匹配会在调用处报错。
- 标志收集参数在调用处可以完全省略 —— 其值默认为一个新实例,
应用所有 `:default` 值(没有默认值的字段为 `nil`)。
### 读取标志值
对于始终有值的字段(有默认值或必需)使用 `~field`,
需要区分“未提供”与假值默认值时使用 `~?field`:
if $opts~push { ... } # bool field — false if defaulted if ($opts~?tag) != nil { ... } # was --tag passed at all?
当字段不存在时,`$m~?field` 返回 `nil`(空值传播,可安全链式调用);
`$m~field` 是普通访问器。
### 标志收集参数必须位于最后
每个函数只允许一个标志收集槽位,且它必须是末尾参数。
在内部,运行时会收集 `--flag value` 键值对,并将生成的
`%TypeName{...}` 实例作为最后一个位置参数追加 —— 因此在其后声明的
任何基本类型参数都永远不会收到值,调用会在运行时抛出错误。
因此规范的签名是 `positional... opts^Opts`:
def deploy {|app^string env^string opts^DeployOpts| # ... } deploy "billing" "prod" --replicas 5 --dry-run # ✗ invalid — deftype parameter is not last def broken {|opts^Opts app^string| ... }
关于类型构造器上的对应功能(`[User --name X --dept Y]`),
见类型与协议(› 关键字参数构造)。
## 闭包
匿名函数使用相同的 `|params| body` 语法,无需 `def`:
def square {|x| ($x * $x) } set also-square {|x| ($x * $x) } # assign closure to a variable [map $xs { ($it * 2) }] # implicit $it param [map $xs { |x| ($x * 2) }] # explicit param
### 隐式 `$it`
没有参数列表的闭包会将参数绑定到 `$it`:
@{ 1 2 3 } |> map { $it * 2 } # $it is 1, then 2, then 3
$xs |> filter { ($it % 2) == 0 }
需要多个参数时,或想要更清晰的名称时,请使用显式参数:
@{ 1 2 3 } |> reduce 0 { |acc x| ($acc + $x) }
### 捕获局部变量
闭包按引用捕获外层局部变量。闭包内的修改会传播回外层作用域:
def counter {|| set count 0 {|| incr count $count } } set bump $counter echo [$bump] # => 1 echo [$bump] # => 2
这对任何内层闭包(`each`、`map`、`filter`、`defer` 等)中的 `<<`、`set`
和 `incr` 都适用。捕获在函数入口处装箱一次 —— 性能说明见
语法与风格。
## 多参数个数分派
一个函数可以有多个函数体,按参数个数选择。每个参数个数各占一个
`{|...| ...}` 块:
def range { {|n| [range 0 $n] } {|start end| [range $start $end] } } [range 5] # => @{0 1 2 3 4} [range 2 5] # => @{2 3 4}
这是 `map`、`filter`、`take`、`drop` 等按参数个数重载的标准库函数的
规范模式 —— 见惰性序列和转换器。
## 装饰器(`@` 前缀元数据)
函数体可以携带以 `@` 为前缀的元标签。它们将元数据附加到函数上,
运行时、工具和你自己的代码都可以通过 `meta` / `info/*` 读回
(见元编程)。
### 保留的元名称
有一小组装饰器名称具有约定俗成的含义。工具(签名显示、帮助栏、
文档渲染器)会专门读取它们:
| 装饰器 | 用途 | 值形式 |
|---|---|---|
| `@doc` | 函数级文档字符串 | 字符串(规范形式) |
| `@returns` | 返回值类型标注(对应 `def name^RetType`) | 字符串或关键字 |
| `@since` | 函数引入时的版本 | 字符串 |
| `@deprecated` | 将函数标记为已弃用 | 布尔值或字符串 |
def distance {|p^Point| @doc "Euclidean distance from origin." @returns :number [math/sqrt (($p~x * $p~x) + ($p~y * $p~y))] } def old-helper {|x| @doc "Use `helper` instead." @deprecated true @since "0.3.0" # ... }
`@returns` 与语法形式 `def name^RetType {|...|}` 会填充元数据中相同的
`"returns"` 键 —— 用哪个更易读就用哪个。可在运行时通过 `meta` 获取:
echo [meta $distance "doc"] # => "Euclidean distance from origin." echo [meta $distance "returns"] # => :number
### 逐参数文档(`@<param-name>`)
名称与已声明参数匹配的装饰器会将自身的值作为该参数的 `:description`
附加到函数签名元数据中(由 `info/sig`、工具提示渲染器和 TUI 帮助栏读取)。
参数名匹配是精确匹配。
def greet {|name^string times^number| @doc "Print a greeting N times." @name "The user's name (required)." @times "How many times to print (defaults to 1)." for i [range $times] { echo "Hello, $name!" } } echo [info/sig "greet"] # => greet(name^string, times^number) | Print a greeting N times.
只有字符串形式会填充参数的 `:description` 槽位。参数名键下的
非字符串值仍会附加到函数的元映射中(在该键下),但不会成为参数描述。
### 自定义标签
其他任何 `@key value` 都是调用方控制的元数据 —— 可用于程序或工具的任何
需要。值可以是任意字面量形式(字符串、数字、布尔值、关键字、映射、
集合、列表、带标签的映射);物化器(materialiser)会将它们转换为相应的
运行时值:
def handler {|req| @doc "Handle an HTTP request." @auth-required true @category :experimental @since 3 @tags @{"v1" "stable"} @config %{ :timeout 30 :retries 3 } # ... } echo [meta $handler "auth-required"] # => true echo [meta $handler "since"] # => 3 echo [meta $handler "tags"](0) # => "v1" echo [meta $handler "config"]~timeout # => 30
重复的键会覆盖 —— 后出现的值生效。函数调用和变量引用不能作为装饰器值
(它们不会在编译期求值,会被静默丢弃);请坚持使用字面量。
要获取某个值的完整元数据映射,请使用 `info/props`:
echo [info/props $handler] # => full metadata map
### RPC 暴露(`@rpc`)
`@rpc true` 标签会将函数发布到运行时的 `RpcRegistry`,以便其他运行时
可以通过 `rpc` 调用它。在所有宿主中可用 —— 见 RPC。
def cmd-open-file {|path^string| @rpc true @doc "Open a file in this editor panel." # ... }
### 异步函数(`@async`)
`@async` 装饰器将 `def` 变为异步函数。调用它会立即返回一个 `Future` ——
函数体作为协程在运行时的事件循环上执行。调用方用 `await` 解析结果:
def fetch-data {|url^string| @async @doc "Fetch a URL concurrently; resolves to the response body." set resp [await [http/get $url]] return $resp~body } set fut [fetch-data "https://api.example.com/data"] echo $fut # => <future:pending> set body [await $fut] # suspends the current coroutine until done echo $body
对 Future 使用 `await`:
- 如果 Future 已经解析,则立即返回,
- 如果仍在等待,则挂起当前协程(而不是整个运行时),
并在 Future 解析时恢复,
- 原样传递非 Future 值,因此可以放心地用于任何值,无论其是否异步。
### 函数体是协程
在 `@async` 函数内部,你可以 `await` 其他 Future,并将控制权
`yield` 给事件循环。这正是让 I/O 并发变得顺手的机制 —— 看起来顺序执行
的代码实际上并发运行:
def aggregate {|urls^list| @async set bodies @{} each { |u| $bodies << [await [fetch-data $u]] # sequential awaits } $urls $bodies }
要实现大规模并行(所有请求同时进行),请将 `@async` 与显式任务派生
配合使用 —— 见并发。
### 调用规则
- 调用 `@async` 函数从不阻塞 —— 调用会在协程的第一个挂起点之后
同步返回一个 Future。
- 带 `@async` 的函数本身是普通函数值;可以传给 `map`、存入映射、
通过 `apply` 调用等。`@async` 只影响调用它所返回的内容。
- 该装饰器同样适用于多参数个数的函数体 —— 每个参数个数
都会成为各自的异步闭包。
- 函数体内部的错误会使 Future 被拒绝,而不是同步传播;
`await` 会在调用处重新抛出它们。
def risky {|x^number| @async if ($x == 0) { [throw "div by zero"] } ($x / $x) } try { [await [risky 0]] # re-throws here, not at [risky 0] } catch e { echo "caught: $e" }
### 生成器(`@generator`)
`@generator` 装饰器将函数体编译为协程生成器 —— 与 `defcoro`
相同的机制。调用该函数不会运行函数体:它会立即返回一个
`<coroutine>` 对象,在第一条语句之前挂起,参数已经绑定。
函数体中的每个 `yield` 向调用方交出一个值;用 `resume`
(或其别名 `next`)驱动生成器:
def range-from {|start^number| @generator @doc "Yield start, start+1, start+2, ..." set n $start while true { yield $n incr n } } set g [range-from 5] # => <coroutine> — body hasn't run yet [coro-state $g] # => :ready [next $g] # => 5 [next $g] # => 6 [resume $g] # => 7 (resume and next are the same)
说明:
- 调用生成器函数从不执行函数体 —— 它只分配协程并绑定参数。
- `yield value` 会挂起函数体并将 `value` 返回给调用方;
裸 `yield` 返回 `nil`。
- 当函数体运行到结束时,下一次 `resume`/`next` 返回 `nil`,
且 `[coro-state $g]` 变为 `:done`。对已完成的生成器调用 `resume`
会一直返回 `nil`,而不会报错。
- 状态就是协程状态:`:ready`(已创建,未启动)、`:suspended`(两次
yield 之间)、`:done`,以及 `:running` 和 `:failed`。如果函数体抛出
异常,错误会在 resume 处被记录(`CORO ERROR`)。
- 所有常规的 `def` 参数语法都可用 —— 类型标注、默认值、
剩余参数 —— 并且生成器闭包本身是普通函数值
(可存储、可传给 `apply` 等)。
- 独立的 `defcoro` 形式与此相同,只是没有 `def` 包装;
`spawn` + `vwait` 在运行时调度器上驱动协程
(见并发)。
### 相关
- 并发 —— `deftask`、`defcoro`、`after`、`sleep`,
以及运行时调度器模型。
- `await`、`defcoro`、`yield` —— 各标准库函数参考见 REPL 内的 `help`
文档。
## 函数值
函数是一等值。可以把它们作为参数传递、存入数据结构、通过 `apply`
调用,或直接按位置调用:
def double {|x| ($x * 2) } [map $xs $double] # pass function value [map $xs double] # bare name also works [apply $double @{5}] # => 10
### `IFn` —— 可调用值
一组固定的内置值类型可以像函数一样调用:关键字、映射、带标签的映射、
集合和向量:
def lookup %{ :name "Alice" :role :admin } echo [$lookup :name] # => Alice — map called with key def users %{ :alice %{ :role :admin } :bob %{ :role :user } } echo [$users :alice] # => %{ :role :admin } echo [:name $lookup] # => Alice — keyword called on map
这是内置分派,而不是用户可扩展的协议:`IFn` 的可调用性固定为上述
几种类型,因此为用户定义类型编写的 `IFn` `defimpl` 永远不会被查询。
用户定义类型的实例只能以通用带标签映射的方式调用 —— `[$user :name]`
读取一个字段。要为自己的类型附加自定义行为,请通过 `defimpl` 实现
某个常规协议并显式调用方法 —— 见类型与协议。
### 用 `curry` 进行部分应用
`curry` 固定前面的参数,返回一个等待其余参数的闭包。
调用结果时,固定的参数会被前置到你传入的内容之前:
def add {|a b| ($a + $b) } set inc [curry $add 1] # fix the first arg echo [$inc 41] # => 42 (calls add 1 41) set ten [curry $add 10] echo [$ten 5] # => 15
`curry` 遵循与 `apply` 相同的解析规则,因此可以传入字符串/关键字名称
来代替函数值:
set greet-bob [curry "greet" "Bob"] $greet-bob # calls (greet "Bob")
返回的闭包本身是普通函数值 —— 可以再次 curry、传给 `map`/`filter`,
或存入数据结构。
## 递归与尾调用
尾调用会被编译为原地循环,因此递归写法在任意深度都安全:
def count-down {|n^number| echo $n if ($n > 0) { count-down ($n - 1) } } count-down 1000000 # does not overflow
对于会耗尽栈的非尾递归,请使用显式累加器或重构为循环。未重构的
非尾递归受 `MAX_CALL_DEPTH` 限制(默认 4096 帧)—— 原地循环优化
仅适用于自递归尾调用。
## 闭包与方法
Deft 不像 Clojure 那样基于第一个参数进行方法分派。要为类型附加行为,
请将 `defimpl` 与协议配合使用 —— 见类型与协议。
deftype Point %{ :x %{} :y %{} } defimpl Point Drawable { draw {|ctx bounds| [draw/cell $ctx $self~x $self~y "*"] } } defimpl Point Measurable { measure {|| %{ :w 1 :h 1 } } }
`$self` 在 impl 函数体内指向接收者。