# 类型与协议
Deft 是动态类型语言,支持可选的类型标注(参见
函数)。为在大规模场景下组织数据与行为,
语言提供了 `deftype`(声明记录/映射形状)、
`defprotocol`(声明方法规格集)、`defimpl`(为类型附加行为)
以及 `defmethod`(向现有实现添加单个方法)。
## 快速开始
# Define a record with typed fields. deftype Point %{ :x %{:type :number} :y %{:type :number} } # Construct by tagged-map literal. set p %Point{ :x 3 :y 4 } # Attach behaviour via a protocol. defprotocol Drawable %{ draw {ctx bounds} } defimpl Point Drawable { draw {|ctx bounds| @doc "Draw an asterisk at the point's coordinates." [draw/cell $ctx $self~x $self~y "*"] } } # Or define a standalone method via defmethod. defmethod Point magnitude {|| [math/sqrt (($self~x * $self~x) + ($self~y * $self~y))] } echo [$p magnitude] # => 5
## `deftype` —— 记录定义
`deftype` 声明一个带字段 schema 的带标签映射。每个字段是一个
描述其类型与元数据的选项映射。
deftype User %{ :name %{:type :string :required true} :email %{:type :string} :age %{:type :number :default 0} :roles %{:type :list :default @{}} :active %{:type :boolean :default true} }
### 字段选项
| 选项 | 用途 |
|---|---|
| `:type` | 类型标注名称(`:string`、`:number`、`:list` 等)或另一个 `deftype` 名称 |
| `:required` | 若为 `true`,字段缺失时构造失败 |
| `:default` | 构造时省略该字段则使用此值 |
| `:description` | 由工具呈现的人类可读描述 |
> 在 schema 映射中使用关键字键(`:type`,而非 `"type"`)。旧文档
> 使用字符串键(`%{ type "string" }`);该形式已弃用。
### 构造
带标签的映射字面量会生成类型化的值:
set alice %User{ :name "Alice" :email "alice@example.com" :roles ^{:admin :user} }
字段访问使用 `~`:
echo $alice~name # => Alice echo $alice~roles # => ^{:admin :user}
`deftype` 本质上就是一个带标签的映射。你可以对标签进行模式匹配,使用
`is?` 或 `satisfies?`:
[is? "User" $alice] # => true [type $alice] # => User
### 关键字参数构造(`[Type --field value ...]`)
带标签的映射字面量(`%User{ :name "Alice" }`)是标准的构造形式,
但对于字段众多、带默认值或有必填约束的类型,关键字参数构造器
更易读、校验更严格。以类型名开头并传入 `--field value` 键值对:
deftype User %{ :first %{:type :string :required true} :last %{:type :string :required true} :dept %{:type :string :default "accounts"} :active %{:type :boolean :default true} } set u [User --first "Alice" --last "Smith"] echo $u~first # => Alice echo $u~dept # => accounts (default applied) echo $u~active # => true (default applied) set v [User --first "Bob" --last "Lee" --dept "eng"] echo $v~dept # => eng (overridden)
任意以 `--` 开头的单个参数都会让整个调用进入关键字参数模式。
在该模式下:
| 规则 | 行为 |
|---|---|
| 字段名 | 每个 `--flag` 必须匹配已声明的字段(或通过 `<<` 继承的字段)。未知标志会直接报错。 |
| `:required` | 强制执行 —— 缺少必填字段会在构造时抛出错误。 |
| `:type` | 校验 —— 值类型不匹配会在构造时抛出错误。 |
| `:default` | 对调用中省略的任何字段生效。 |
| 布尔简写 | 后面不跟值的裸 `--flag` 表示 `true`(仅对 `:boolean` 字段有效)。 |
| 位置参数 | 不允许。一旦用 `--` 进入该模式,所有参数都必须是标志。 |
[User --first "a" --last "b" --bogus "x"] # ✗ unknown field '--bogus' [User --first "only"] # ✗ missing required ':last' [User "posfirst" --last "poslast"] # ✗ positional args rejected in kwarg mode [Toggle --admin] # ✓ boolean shortcut → admin=true [Toggle --admin false --disabled true] # ✓ explicit boolean values
位置构造器(`[Point 10 20]`)保持不变,当字段顺序明确且稳定时仍是
合适的选择 —— 通常用于 `Point` 或 `Size` 这类小型、天然按位置排列
的记录。当字段数 ≥3–4 个、多个字段带默认值,或调用处的可读性
很重要时,应改用关键字参数形式。
### 与标志收集参数的关系
关键字参数构造器与函数级的标志收集参数
(参见 函数,› 命名可选参数)共享相同的
标志语法和相同的 schema。末尾的 `opts^Opts` 参数会被脱糖为
用末尾的 `--flag value` 参数构造 `%Opts{...}`,因此凡是用类型的
关键字参数构造器能表达的内容,在函数调用处也以同样方式生效:
deftype DeployOpts %{ :replicas %{:type :number :default 1} :dry-run %{:type :boolean :default false} } def deploy {|app^string opts^DeployOpts| echo "deploying $app with $opts~replicas replicas" } deploy "billing" --replicas 5 --dry-run # opts is built as if by: %DeployOpts{ :replicas 5 :dry-run true }
### 扩展现有类型
`<<` 允许一个类型继承另一个类型的字段(镜像追加运算符 —— 它
用父类型的字段更新左侧的类型):
deftype Animal %{ :name %{:type :string} } deftype Dog << Animal %{ :breed %{:type :string} } set rex %Dog{ :name "Rex" :breed "Malinois" } echo $rex~name # => Rex
## `defprotocol` —— 方法规格集
协议是一组命名的方法规格(名称 + 元数)。`defimpl`
方法会在加载时依据这些规格进行校验。
defprotocol Drawable %{ draw {ctx bounds} } defprotocol Measurable %{ measure {} natural-height {} } defprotocol Focusable %{ accepts-focus? {} on-focus {} on-blur {} }
方法规格块使用相同的 `%{...}` 映射语法。每一项是一个方法名
及其期望的参数名。参数上允许使用类型标注:
defprotocol Gridable %{
row-count {}
col-count {}
cell {row col value opts^CellOpts}
}
规格块中 `%{...}` 与 `^{...}` 均可使用。
### 规格块不可或缺
已声明的方法规格(名称 + 元数)会注册到类型注册表,而不仅
仅是协议名。`defimpl` 方法会依据这些规格进行校验 —— 不属于该
协议的方法(几乎总是拼写错误,例如 `onkey` 与 `on-key`)会在
加载时记录警告,而不是静默创建一个不可达的方法。
defprotocol Keyable %{ on-key {event bounds} } defimpl MyWidget Keyable { on-key {|event bounds| ... } # ✓ matches spec onkey {|event bounds| ... } # ⚠ warned — not in spec }
分派本身仍基于名称(`resolveMethod(type, "on-key")`);
这些规格用于校验和 `satisfies?`,而非运行时路由。
## `defimpl` —— 附加行为
`defimpl <Type> <Protocol> { ... }` 在单个块中为一个类型定义
某个协议的全部方法:
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 } } natural-height {|| 1 } }
实现体中的 `$self` 指接收者。方法根据接收者的类型标签
进行分派。
### 分派
调用按名称 + 接收者类型分派。使用方括号形式调用:
def p %Point{ :x 3 :y 4 } [draw $p $ctx $bounds] # routes to Point's draw method [$p measure] # also works (postfix-style)
### 多协议实现
当方法不冲突时,你也可以在一个块中实现多个协议:
defimpl MyWidget [Drawable Keyable] {
draw {|ctx bounds| ... }
on-key {|event bounds| ... }
}
> 优先为每个协议使用单独的 `defimpl` 块 —— 这样更清晰,
> 代价是每个协议多写一行。
### 内置协议
Deft 随附了许多内置协议。控件级协议
(`Drawable`、`Measurable`、`Hittable`、`Keyable`、`Container`、
`Textable`、`Editable`、`Selectable`、`Focusable`、`Scrollable`)
声明在 `packages/ui/src/protocols.dft` 中,被 TUI 应用广泛使用。
参见 TUI:控件与协议。
四个面板级协议(`Panel`、`Focusable`、`Scrollable`、
`AppLifecycle`)在 TUI 启动时由 Zig 注册,并以同样方式
携带规格:
| 协议 | 方法 |
|---|---|
| `Panel` | `draw {ctx}`, `on-event {event}`, `layout {bounds}`, `children {}` |
| `Focusable` | `on-focus {}`, `on-blur {}`, `accepts-focus? {}` |
| `Scrollable` | `on-scroll {delta}`, `scroll-offset {}` |
| `AppLifecycle` | `on-init {}`, `on-suspend {}`, `on-resume {}`, `on-deinit {}` |
外加任何用户自定义协议,它们都以同样方式携带规格。
## `defmethod` —— 单方法定义
`defmethod` 向已有(或将来)的实现添加一个方法。当某个方法
大到需要独立的文件或作用域时很有用。
defmethod Point magnitude {|| [math/sqrt (($self~x * $self~x) + ($self~y * $self~y))] } defmethod Point describe {|| "Point($self~x, $self~y)" }
参数位于 `|...|` 内。零参数使用 `||`。
## `satisfies?` —— 协议成员关系
`satisfies?` 返回值所属类型是否实现了某个协议:
def p %Point{ :x 3 :y 4 } [satisfies? $p "Drawable"] # => true [satisfies? $p "Measurable"] # => true (if defined) [satisfies? $p "Container"] # => false
## 带标签的映射
`deftype` 本质上就是带标签的映射 —— 一个带有前置标签的映射。
你也可以不声明 `deftype` 而直接构造带标签的映射:
set ok-result %Ok{ :value 42 } set err-result %Err{ :message "broken" } [is? "Ok" $ok-result] # => true [type $ok-result] # => Ok match $result { %Ok{ :value $v } => $v %Err{ :message $m} => [throw $m] }
### 标签辅助函数
[tag $somemap :Foo] # add or change a tag [untag $foo-map] # strip the tag
`Ok` 和 `Err` 是 Result 类型使用的标准带标签映射 ——
参见 错误处理。
## 类型内省
用于在没有标注时进行运行时类型检查:
[type 42] # => "number" [type "hi"] # => "string" [type @{ 1 2 }] # => "list" [type %Point{...}] # => "Point" [is? "number" 42] # => true — type name as a STRING [is? "Point" $p] # => true [nil? $x] # => true if $x is nil [empty? $xs] # => true if $xs is empty
> `is?` 接受类型名作为字符串 —— `is? "number" x`,而非
> `is? :number x`。关键字形式已不再支持。
## 比较:类型标注与 `deftype`
| 方面 | 类型标注 | `deftype` |
|---|---|---|
| 目的 | 标注参数 / 变量 | 定义数据结构 |
| 作用域 | 单个 def | 整个模块 |
| 校验 | 函数调用时 | 构造时 |
| Schema | 单个类型名 | 每字段的选项映射 |
| 复用 | 内联 | 可跨函数复用 |
二者互补 —— `deftype` 定义结构,类型标注在函数边界引用它。
## 另见
- 函数 —— 参数语法与 `^` 标注
- 模式匹配 —— 解构 `%Type{...}` 与 `%{...}`
- 元编程 —— `info/types`、`info/protocols`、反射
- TUI 控件与协议 —— 内置 UI 协议