# 函数 函数是 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 函数体内指向接收者。