# 类型与协议 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 协议