:Adapter、关联类型与参数化接口的设计与演进)
Carbon 泛型细节二Adapter、关联类型与参数化接口的设计与演进【免费下载链接】carbon-langCarbon Languages main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang导读本文基于 Carbon Language 仓库中的提案 p000731-generics-details-2-adapters-associated-types-parameterized-interfaces.md系统讲解 Carbon 泛型设计中三个核心机制适配器adapter、关联常量与关联类型associated constants / associated types、以及参数化接口parameterized interfaces。这是继泛型目标#24、泛型术语#447、泛型总览#524与泛型细节第一部分#553之后的系列提案之一最终内容落地于 docs/design/generics/details.md。读完本文你将理解这些机制要解决什么问题、为什么采用当前的语法决策、以及它们的实现与编译期查证逻辑在仓库源码中如何体现。提案背景与定位Carbon 希望提供高质量泛型能力目标见 泛型目标提案但这一特性过于庞大无法在一次提案中全部落地因此被拆分为一系列提案逐步细化#24: Generics goals——确立泛型特性目标#447: Generics terminology——统一泛型术语#524: Generics overview——给出泛型特性的高层描述与文档导航#553: Generics details part 1——泛型细节第一部分本提案#731——继续细化adapter、关联类型与其他常量、参数化接口三块内容后续还有 泛型细节 3constraints 等继续推进。本提案的内容最初提取自一个更大的 Generics combined draft proposal具体做法是向 docs/design/generics/details.md 这个设计文档新增多个章节。该文档当前已包含完整的 Adapting types、Associated constants、Associated facets 与 Parameterized interfaces 章节正是本提案结论的延续与落地。三大核心主题概述提案将泛型细节的第二批内容划分为三个主题主题核心问题关键语法机制适配器adapters同一类型只能实现接口一次且实现位置受限如何为值切换接口视图adapt关键字、extend adapt、impl as ... ...关联常量 / 关联类型接口签名中的类型如何随实现变化接口内let常量、where子句赋值参数化接口如何表达一族相关接口允许同一类型多次实现接口名后参数列表如Stack(ElementType: type)这三个机制共同服务于一个目标让checked generics的函数签名能够表达任意实现了某接口的类型而不必写出具体类型同时保持编译期可查证详见 泛型术语文档。关联常量与关联类型语法决策使用let声明编译期常量关联常量associated constants指的是接口中除方法之外的其他成员它们由接口的实现者提供具体值。提案指出问题 #739: Associated type syntax 的 let 常量部分保持一致interface Stack { let ElementType:! Type; fn Pushaddr me: Self*; ... } class DynamicArray(T:! Type) { ... impl as Stack { let ElementType:! Type T; fn Pushaddr me: Self*; ... } }这里ElementType就是典型的关联类型接口声明它存在具体类型如DynamicArray(T)在实现Stack时把它绑定为T。用auto自动推导类型如果不想手写约束可以把类型位置换成auto由编译器根据右侧的值自动确定class DynamicArray(T:! Type) { ... impl as Stack { let ElementType:! auto T; fn Pushaddr me: Self*; ... } }这种写法的价值在于接口约束演化时减少改动当接口的约束被放宽或收紧时只要右侧的值仍满足新约束impl本身无需修改当约束收紧时只需修改不满足新约束的实现再修改接口本身。备选方案一省略类型声明曾考虑在impl中省略类型、始终使用接口中声明的类型class DynamicArray(T:! Type) { ... impl as Stack { let ElementType T; // 无类型标注 fn Pushaddr me: Self*; ... } }该方案在接口约束变化时改动更少但无法增量地强化约束。最终选择显式把约束写进实现虽然在某些情况下会产生更多噪音例如新增约束时即使所有实现已满足也要逐一更新但好处是获得更多工具来增量式地推进接口约束的变更因此被采纳提案也明确表示若实践表明这是糟糕的权衡应当重新评估。备选方案二从方法签名推断关联类型Swift 方案被拒绝Swift 允许在方法签名可推导时省略关联类型的值见 Swift 官方文档对关联类型的描述。例如上面的例子只需从上下文推断ElementType Tclass DynamicArray(T:! Type) { ... impl as Stack { // 不需要写: let ElementType:! Type T; fn Pushaddr me: Self*; ... } }好处是接口新增关联类型时无需修改所有实现。但提案指出这在存在带默认实现的方法重载时会复杂化例如interface Has2OverloadsWithDefaults { let T:! StackAssociatedType; fn Fme: Self, y: T) { ... } fn Fme: Self { ... } } class S { impl as Has2OverloadsWithDefaults { // 无法确定 T 是 DynamicArray(Int) 还是 // DynamicArray(DynamicArray(Int)). fn Fme: Self), y: DynamicArray(Int)) { ... } } }Swift 曾因关联类型推断是唯一需要全局类型推断的特性而考虑移除后来决定保留。Carbon 认为它带来推断复杂度且并非必要因此仅做了简短讨论便未采纳。落地现状where子句与关联常量需要说明的是语法在设计演进中有所调整。当前 details.md 中关联常量使用let声明、通过where子句赋值。例如固定维度的点类型interface NSpacePoint { let N: i32; // 以下方法要求: 0 i N。 fn Get(ref self, i: i32) - f64; fn Set(ref self, i: i32, value: f64); // 关联常量可用于签名: fn SetAll(ref self, value: Array(f64, N)); }实现方通过where .N 2等语法为关联常量赋值class Point2D { extend impl as NSpacePoint where .N 2 { fn Get(ref self, i: i32) - f64 { ... } fn Set(ref self, i: i32, value: f64) { ... } fn SetAll(ref self, value: Array(f64, 2)) { ... } } }关联常量还有两条硬性约束不能为final关联常量指定值没有默认值的关联常量每个实现都必须指定。多个赋值可以用and连接。这些值可作为类型成员直接访问如Point2D.N 2也可在 checked-generic 函数体内使用如PointT.N作为数组长度。关联常量也可以是函数称为关联函数associated functions通过接口内的fn声明例如反序列化接口interface DeserializeFromString { fn Deserialize(serialized: String) - Self; } class MySerializableType { var i: i32; extend impl as DeserializeFromString { fn Deserialize(serialized: String) - Self { return {.i StringToInt(serialized)}; } } } var x: MySerializableType MySerializableType.Deserialize(3);这里没有使用用let声明函数类型常量的写法而是直接用fn以与类成员函数的声明语法保持一致见 classes.md。关联 Facet让方法签名随实现变化如果关联常量的类型本身是 facet 类型就得到关联 facetassociated facets。它们的价值在于可出现在关联方法或函数的签名中使方法签名随实现而变化。仓库中典型的例子是栈接口interface StackAssociatedFacet { let ElementType: type; fn Push(ref self, value: ElementType); fn Pop(ref self) - ElementType; fn IsEmpty(ref self) - bool; }DynamicArray(T)实现它时把ElementType绑定到Tclass DynamicArray(T: type) { ... extend impl as StackAssociatedFacet where .ElementType T { fn Push(ref self, value: ElementType) { self.Insert(self.End(), value); } fn Pop(ref self) - ElementType { var pos: IteratorType self.End(); Assert(pos ! self.Begin()); --pos; returned var ret: ElementType *pos; self.Remove(pos); return var; } fn IsEmpty(ref self) - bool { return self.Begin() self.End(); } } }有了这个接口就能写出不依赖具体类型的 checked-generic 函数fn PeekAtTopOfStackStackType: StackAssociatedFacet - StackType.ElementType { var top: StackType.ElementType s-Pop(); s-Push(top); return top; }从 details.md 的说明看在 checked-generic 函数内部StackType.ElementType是一个 archetype原型类型其 API 由接口中的声明决定而在泛型之外关联 facet 由 impl 查找得到具体值——例如对DynamicArray(i32)StackType.ElementType就是i32。这支撑了 泛型目标文档 中泛型函数可替代普通函数而不改变调用者所见返回类型的目标。关联 facet 还可以用**成员类型member type**实现。此外 terminology.md 用输入/输出模型给出了清晰的区分接口参数是输入必须先指定才能确定impl关联常量是输出由impl决定、不参与impl选择。例如容器的迭代器类型由容器自身决定正适合作为关联常量。参数化接口一族接口与多重实现基本形态与每种参数一种实现关联常量不改变一个类型最多实现一个接口一次的事实。若想表达一族相关接口同一类型可为不同参数值提供多个实现就需要参数化接口写法是接口名后跟参数列表interface StackParameterized(ElementType: type) { fn Push(ref self, value: ElementType); fn Pop(ref self) - ElementType; fn IsEmpty(ref self) - bool; }此时StackParameterized(Fruit)与StackParameterized(Veggie)被视为不同的接口、拥有独立的实现。一个类型可以同时实现它们class Produce { var fruit: DynamicArray(Fruit); var veggie: DynamicArray(Veggie); extend impl as StackParameterized(Fruit) { fn Push(ref self, value: Fruit) { self.fruit.Push(value); } fn Pop(ref self) - Fruit { return self.fruit.Pop(); } fn IsEmpty(ref self) - bool { return self.fruit.IsEmpty(); } } extend impl as StackParameterized(Veggie) { fn Push(ref self, value: Veggie) { self.veggie.Push(value); } fn Pop(ref self) - Veggie { return self.veggie.Pop(); } fn IsEmpty(ref self) - bool { return self.veggie.IsEmpty(); } } }接口参数不可推导与接口中的关联常量、类型参数不同接口参数不能被推导。改写上面PeekAtTopOfStack的例子会直接产生编译错误// ❌ 错误: 无法推导接口参数 T。 fn BrokenPeekAtTopOfStackParameterized [T: type, StackType: StackParameterized(T)] (s: StackType*) - T { ... }原因在于编译器无法确定传入Produce*时T应该是Fruit还是Veggie。解决办法有二要么把T替换成具体类型fn PeekAtTopOfFruitStack [StackType: StackParameterized(Fruit)] (s: StackType*) - T { ... } var produce: Produce ...; var top_fruit: Fruit PeekAtTopOfFruitStack(produce);要么显式传递T配合where约束详见 details.md 中Another type implements parameterized interface小节fn PeekAtTopOfStackParameterizedImpl (generic T: type, generic StackType: StackParameterized(T), s: StackType*) - T { ... } fn PeekAtTopOfStackParameterized[StackType: type] (s: StackType*, generic T: type where StackType impls StackParameterized(T)) - T { return PeekAtTopOfStackParameterizedImpl(T, StackType, s); }运算符重载与多重实现参数化接口对运算符重载尤其有用EqWith(T)、OrderedWith(T)这类接口允许一个类型与多个其他类型比较。例如interface EqWith(T: type) { fn Equal(self, rhs: T) - bool; ... } class Complex { var real: f64; var imag: f64; // 只要参数不同可以多次实现同一接口 extend impl as EqWith(f64) { ... } // 等价于: impl as EqWith(Complex) { ... } extend impl as EqWith(Self) { ... } }接口参数默认都是 checked 参数因为它们在编译期就必须解析且允许传入 symbolic 或 template 值。接口参数也不要求一定是 facet 类型只是绝大多数情况如此——例如把元组成员读取操作建模为以index为参数的接口interface ReadTupleMember(index: u32) { let T: type; // 返回 self[index] fn Get(self) - T; }同一参数值不可实现两次Map 与 Bijection 的教训当同一类型对相同参数组合实现了两次同一接口时会产生编译错误interface Map(FromType: type, ToType: type) { fn Map(ref self, needle: FromType) - Optional(ToType); } class Bijection(FromType: type, ToType: type) { extend impl as Map(FromType, ToType) { ... } extend impl as Map(ToType, FromType) { ... } } // ❌ 错误: Bijection 对接口 Map(String, String) 有两个不同的 impl 定义 var oops: Bijection(String, String) ...;当FromType ToType时两个 impl 冲突。文档给出的修复方案正是使用适配器容纳反向查找的 implclass Bijection(FromType: type, ToType: type) { extend impl as Map(FromType, ToType) { ... } } class ReverseLookup(FromType: type, ToType: type) { adapt Bijection(FromType, ToType); extend impl as Map(ToType, FromType) { ... } }参数化命名约束不仅接口可以参数化命名约束named constraints也支持参数其语义与接口参数一致详见 details.md 的 Parameterized named constraints 小节。Adapter适配器为类型切换接口视图为什么需要 adapter由于接口对同一类型最多实现一次且实现位置受到限制即孤儿规则的约束见 details.md用户需要一种切换值的类型以访问不同接口实现的手段。Carbon 因此提供 adapter创建与既有类型兼容、但 API尤其接口实现集合不同的新类型。仓库的典型示例interface Printable { fn Print(self); } interface Ordered { fn Less(self, rhs: Self) - bool; } class Song { extend impl as Printable { fn Print(self) { ... } } } class SongByTitle { adapt Song; extend impl as Ordered { fn Less(self, rhs: Self) - bool { ... } } } class FormattedSong { adapt Song; extend impl as Printable { fn Print(self) { ... } } } class FormattedSongByTitle { adapt Song; extend impl as Printable FormattedSong; extend impl as Ordered SongByTitle; }可以看到 adapter 支持三种典型用法为原类型补充新接口实现SongByTitle、提供同一接口的不同实现FormattedSong、以及从其他兼容类型组合复用实现FormattedSongByTitle用impl as ... ...语法直接复用。adapter 的完整定义可添加哪些声明、兼容规则、成员访问、类型间转换见 classes.md 的 adapters 章节。Adapter 兼容性HashMap 的例子考虑一个带 facet 参数的类型如哈希表interface Hashable { ... } class HashMap(KeyT: Hashable, ValueT: type) { fn Find(self, key: KeyT) - Optional(ValueT); // ... }由于KeyT、ValueT是 checked 参数Find只能使用参数类型被声明要求的那些能力。基于这一点可以判定两个 adapter 之间何时允许转换。设有两个Song的 adapterclass PlayableSong { adapt Song; extend impl as Hashable Song; // 复用 Song 的 Hashable 实现 extend impl as Media { ... } } class SongHashedByTitle { adapt Song; extend impl as Hashable { ... } // 不同的 Hashable 实现 }Song与PlayableSong不仅数据表示相同Hashable的实现也相同因此HashMap(Song, i32)与HashMap(PlayableSong, i32)之间可以显式转换而SongHashedByTitle的哈希实现不同虽然Song与SongHashedByTitle是兼容类型但对应的HashMap类型不兼容——因为 HashMap 的不变量依赖哈希函数保持不变。扩展 adapterextend adapt多数情况下 adapter 希望保留原类型的大部分 API最常见的是新增或替换某个接口实现。用extend前缀修饰adapt即可从原类型既有 API 出发extend同时扩展成员访问与 impl 查找见 member_access.mdclass SongByArtist { extend adapt Song; // 新增一个接口实现 extend impl as Ordered { ... } // 用另一种实现替换既有实现 extend impl as Hashable { ... } }结果SongByArtist实现了OrderedSong没有、实现了Hashable但不同于Song、继承了Song的Printable。其规则是查找SongByArtist是否实现接口I时若未找到编译器会继续查看Song是否实现I找到则尽可能复用——只要接口函数签名中引用Self的类型都能相应替换转换成功。需要注意class B { extend base: A; }的类扩展中基类不能是 final但class B { extend adapt A; }在A是 final 类时也允许。与普通adapt一致B到A没有隐式转换。当接口间出现名字冲突时可以去掉extend实现接口再用alias单独引入或重命名所需名字class SongRenderToPrintDriver { extend adapt Song; // 新增一个 Print() 成员函数 fn Print(self) { ... } // 与新的 Print 避免名字冲突: // 以非 extend 方式实现 Printable impl as Printable Song; // 把 Printable.Print 以 PrintToScreen 名字暴露 alias PrintToScreen Printable.Print; }实战用例一组合独立开发的库两个包CompareLib定义CompareLib.Comparable接口与 checked-generic 算法CompareLib.Sort与SongLib定义类型SongLib.Song彼此无依赖因此任何一方都不会为对方定义实现。用户可定义一个 adapter 为SongLib.Song提供CompareLib.Comparable实现import CompareLib; import SongLib; class Song { extend adapt SongLib.Song; extend impl as CompareLib.Comparable { ... } } // 或者不把 CompareLib.Comparable 的名字混入 Song 的 API: class Song { extend adapt SongLib.Song; } impl Song as CompareLib.Comparable { ... }调用时既可以把SongLib.Song显式转换为Song也可以直接使用Song值var lib_song: SongLib.Song ...; CompareLib.Sort((lib_song as Song,)); var song: Song ...; CompareLib.Sort((song,));实战用例二为其他类型提供可复用实现可以定义一个以被适配类型为参数的 adapter实现某个接口再通过impl as ... ...语法把它拉进来复用。例如为所有实现了Difference接口的类型提供Comparableinterface Comparable { fn Less(self, rhs: Self) - bool; } interface Difference { fn Sub(self, rhs: Self) - i32; } class ComparableFromDifference(T: Difference) { adapt T; extend impl as Comparable { fn Less(self, rhs: Self) - bool { return (self as T).Sub(rhs) 0; } } } class IntWrapper { var x: i32; impl as Difference { fn Sub(self, rhs: Self) - i32 { return left.x - right.x; } } impl as Comparable ComparableFromDifference(IntWrapper); }实战用例三私有实现Private impl当库公开某个类型、但只想把该类型实现了某接口作为内部实现细节时可为该类型创建私有 adapter 并在其上实现接口成员方法通过把self转换到 adapter 类型来使用该私有实现// 公开位于 API 文件 class Complex64 { // ... fn CloserToOrigin(self, them: Self) - bool; } // 私有 class ByReal { extend adapt Complex64; // 复数通常不可比较但这个比较函数对某些方法实现很有用。 extend impl as Comparable { fn Less(self, that: Self) - bool { return self.Real() that.Real(); } } } fn Complex64.CloserToOrigin(self, them: Self) - bool { var self_mag: ByReal self * self.Conj() as ByReal; var them_mag: ByReal them * them.Conj() as ByReal; return self_mag.Less(them_mag); }实战用例四便捷访问接口名字如果函数要调用某接口的多个函数而类型并未extend该接口的实现每次都要使用限定成员访问会比较啰嗦。adapter 可以把实现了该接口变成类型本身 API 的一部分interface DrawingContext { fn SetPen(self, ...); fn SetFill(self, ...); fn DrawRectangle(self, ...); fn DrawLine(self, ...); ... } impl Window as DrawingContext { ... } class DrawInWindow { adapt Window; extend impl as DrawingContext Window; } fn Render(w: Window) { let d: DrawInWindow w as DrawInWindow; d.SetPen(...); d.SetFill(...); d.DrawRectangle(...); ... }细节文档还提示也可以通过局部 symbolic facet 常量达到同样效果let generic DrawInWindow: Draw Window;这属于另一条路径。源码层面的印证adapter 并非纸上设计在工具链实现中已有明确落点toolchain/check/class.cpp 负责校验 adapter 定义的合法性定义了AdaptWithBaseadapter 带基类、AdaptWithFieldsadapter 带字段、AdaptWithVirtualadapter 带虚函数等诊断错误同时规定 adapter 的对象表示object representation就是被适配类型的对象表示toolchain/check/convert.cpp 在类型转换逻辑中处理 base 与 adapt 关系包括 tuple/struct 的逐部分转换以及沿 adapter 链走到被适配类型的转换路径。这印证了adapter 是对象表示相同、接口视图不同的语义且相关规则已被编译器实现与诊断覆盖。被否决的备选方案与理由为什么是adapter而不是adaptor两种拼写都有依据但-er拼写在英文文本和代码中更常见且 GoF《设计模式》一书采用-er拼写adapter pattern因此最终选定adapter。值模式Value patterns被否决曾考虑允许函数参数使用不带:的值模式以便把T绑定到参数列表中较后出现的类型fn PeekAtTopOfStackParameterized [T:! Type, StackType:! StackParameterized(T)] (s: StackType*, T) - T { ... }但 Carbon 不希望普遍开放值模式——否则fn F(Int)这类声明会被接受而用户几乎总是想写fn F(i: Int)。为保留对这类笔误的报错能力该方案被否决。可推导接口参数被否决及其与一致性coherence的关系曾考虑区分两种接口参数multi 参数即现在的参数化接口参数与deducible可推导类型参数。后者只允许一个类型对接口有一种实现可像关联类型一样被推断fn PeekAtTopOfStack[ElementType:! Type, StackType:! Stack(ElementType)] (s: StackType*) - ElementType { ... }提案给出了系统的否决理由只有一种参数使语言更简单multi 参数表达了确实需要的东西而可推导参数总能改写为关联类型每个接口 × 参数组合一种实现与其他参数化构造如Foo(A)与Foo(B)是两个不同且无关的类型更一致难以给出何时用关联类型、何时用可推导参数的清晰指引结构接口中的可推导参数需要额外规则确保无歧义推导。最关键的是可推导接口参数会复杂化 impl 的查找规则并可能破坏一致性coherence见 docs/design/generics/goals.md。提案用一组包/库的例子说明问题假设X库定义了接口I(T)与类型A而Y库为X.I(Y.T1)实现X.A、Z库为X.I(Z.T2)实现X.Apackage X library I and A api; interface I(Type:$ T) { ... } struct A { ... }package Y library T1 api; import X library I and A; struct T1 { ... } // 类型 X.A 对 X.I(T) 有实现其中 T Y.T1。 impl X.I(T1) for X.A { ... }package Z library T2 api; import X library I and A; struct T2 { ... } // 类型 X.A 对 X.I(T) 有实现其中 T Z.T2。 impl X.I(T2) for X.A { ... }package Main api; import X library I and A; // 考虑如果组合包含下面两句的不同组合会怎样: // import Y library T1; // import Z library T2; // 函数 F 用值 a类型 U调用其中 U 对某个 T 实现了接口 X.I(T)。 fn FType:$ T, X.I(T):$ U { ... } fn Main() { var X.A: a X.A.Init(); F(a); }调用F(a)会触发对接口X.I(T)的查找而Y.T1与Z.T2两个库中存在针对不同T的实现由此带来一系列问题只导入你使用的难以度量Y、Z除了import语句外从未被提及却影响着行为F(a)的解释取决于导入组合都不导入时报错都导入时产生歧义只导入一个时执行的代码完全不同无法强制每接口一种实现规则来消除歧义。本质上如果允许接口参数被推导就无法保证导入那些定义了接口参数所用类型的库。接口实现是 Carbon 中唯一允许开放扩展open extension的语言构造是解决表达式问题的关键但必须限制哪些库能为类型实现接口以保证使用时必然能看到实现——这正是本提案没有采用可推导接口参数的根本原因。只保留关联类型、不要接口参数Swift 路线被否决Swift 只使用关联类型但这样无法用接口表达运算符重载——例如向量既应能与向量相加、也应能与点相加。因此 Carbon 跟随 Rust在关联类型之外还提供 trait接口参数并以此定义运算符行为。与其他语言的横向对比小结机制CarbonRustSwift关联类型关联常量 / 关联 facetletwhereassociated typesassociated types接口参数参数化接口checkedgeneric traits无只有关联类型关联类型推断不支持需显式或auto不支持支持曾考虑移除运算符重载建模参数化接口如EqWith(T)泛型 trait 运算符重载运算符重载受限于关联类型Rust 术语中 interface 参数与关联 facet 都叫 type parameters但 Carbon 沿用了 Rust RFC 0195 的区分接口参数是输入决定选择哪个 impl关联常量是输出由 impl 决定、不参与选择。结论与延伸阅读本提案确立了 Carbon 泛型细节三块地基adapter 提供类型视图切换、关联常量让接口签名随实现变化、参数化接口表达一族可多重实现的接口。围绕adapter、let :!常量、where赋值等语法选择提案记录了完整的设计权衡过程尤其是对 Swift 关联类型推断与可推导接口参数的否决以及对一致性/coherence 的守护。当前设计文档中的语法在细节上有进一步演进如关联常量通过where子句赋值但核心概念与取舍一脉相承且 adapter 的合法性检查已在 toolchain/check/class.cpp 等编译器源码中落地。继续深入可参考以下仓库文档设计细节全文docs/design/generics/details.md含 adapting types、associated constants、associated facets、parameterized interfaces 各节高层总览docs/design/generics/overview.md术语澄清docs/design/generics/terminology.mdInterface parameters and associated constants一节泛型目标docs/design/generics/goals.md类与 adapter 的完整定义docs/design/classes.md系列后续泛型细节 3constraints【免费下载链接】carbon-langCarbon Languages main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考