Shapeless是否支持特质类型成员的Generic自动派生?
我尝试使用shapeless为特质中定义的类型成员派生Generic实例,但始终未能成功。我在尽可能贴近原有代码逻辑的前提下,构造了最简问题复现代码。我参考了相关博客的实现思路,尝试在其基础上扩展实现更通用的能力(改造过程可能偏离了原作者的设计思路)。单看这个示例可能无法理解我为什么要设计这样的代码结构,但希望这不会影响问题本身的表述:)
基础类型定义
我首先定义了一个声明类型成员的特质、一个表示公共字段集合的样例类,以及一个将两者实例组合起来的包装样例类:
object A { trait TheTrait { type TheType } case class CommonFields(height: Double, isTall: Boolean) case class Wrapper[T <: TheTrait](commonFields: CommonFields, t: T#TheType) }
同时编写了该特质的一个实现:
trait Obj extends TheTrait object Obj extends Obj { case class Source(name: String, other: Int) override type TheType = Source }
实现目标
传入一个包含某TheTrait实例下CommonFields和TheTrait#TheType所有字段值的元组,借助shapeless将其转换为Wrapper实例。以上述示例代码为例,希望能将形如(5.1, false, "sub", 10)的元组转换为Wrapper[Obj](CommonFields(5.1, false), Source("other", 10))。
初始实现与报错
最初编写的实现代码如下:
object Test { class Constructor[T <: TheTrait] { // 接收包含CommonFields和T#TheType所有字段的元组,生成对应的Wrapper实例 def apply[In <: Product, All <: HList, ORep <: HList, CRep <: HList, N <: Nat](in: In)(implicit cGen: Generic.Aux[A.CommonFields, CRep], // CommonFields的Generic实例 cLen: Length.Aux[CRep, N], // CommonFields对应HList的长度 trGen: Generic.Aux[T#TheType, ORep], // T#TheType的Generic实例 iGen: Generic.Aux[In, All], // 输入元组的Generic实例 split: Split.Aux[All, N, CRep, ORep] // 将输入HList按N长度拆分为CommonFields和T#TheType对应部分 ): A.Wrapper[T] = { val all = iGen.to(in) val (cFields, tFields) = split(all) val com = cGen.from(cFields) val tr = trGen.from(tFields) A.Wrapper(com, tr) } } def construct[T <: TheTrait] = new Constructor[T] println(construct[Obj](5.1, false, "sub", 10)) }
代码无法找到所需的正确隐式实例,具体报错为:
No implicit arguments of type: hlist.Split.Aux[Double :: Boolean :: String :: Int :: HNil, Succ[Succ[shapeless.nat._0]], Double :: Boolean :: HNil, HNil]
从报错信息来看,编译器已经正确找到了CommonFields对应的泛型表示(从报错中出现的Double :: Boolean :: HNil可以判断),但无法确定TheType对应的类型。
第一次修改:改用路径依赖类型
为了验证问题原因,尝试改用路径依赖类型而非类型投影来编写变体实现,新增传入T类型的实例参数:
object Test { import A._ class Constructor[T <: TheTrait] { // 接收包含CommonFields和T#TheType所有字段的元组,生成对应的Wrapper实例 def apply[In <: Product, All <: HList, ORep <: HList, CRep <: HList, N <: Nat](in: In, t: T /* 新增传入T类型实例 */)(implicit cGen: Generic.Aux[CommonFields, CRep], // CommonFields的Generic实例 cLen: Length.Aux[CRep, N], // CommonFields对应HList的长度 trGen: Generic.Aux[t.TheType, ORep], // T#TheType的Generic实例,不再使用类型投影 iGen: Generic.Aux[In, All], // 输入元组的Generic实例 split: Split.Aux[All, N, CRep, ORep] // 将输入HList按N长度拆分为CommonFields和T#TheType对应部分 ): Wrapper[T] = { val all = iGen.to(in) val (cFields, tFields) = split(all) val com = cGen.from(cFields) val tr = trGen.from(tFields) Wrapper(com, tr) } } def construct[T <: TheTrait] = new Constructor[T] println( construct[Obj]((5.1, false, "sub", 10), Obj) // 传入TheTrait实例 ) }
依然出现相同报错:
No implicit arguments of type: hlist.Split.Aux[Double :: Boolean :: String :: Int :: HNil, Succ[Succ[shapeless.nat._0]], Double :: Boolean :: HNil, HNil]
第二次修改:调整隐式参数顺序
调整隐式参数的声明顺序后问题有了一定进展:编译器不再将ORep推断为HNil,而是可以正确识别其需要匹配String :: Int :: HNil:
class Constructor[T <: TheTrait] { // 接收包含CommonFields和T#TheType所有字段的元组,生成对应的Wrapper实例 def apply[In <: Product, All <: HList, ORep <: HList, CRep <: HList, N <: Nat](in: In, t: T)(implicit cGen: Generic.Aux[CommonFields, CRep], // CommonFields的Generic实例 cLen: Length.Aux[CRep, N], // CommonFields对应HList的长度 iGen: Generic.Aux[In, All], // 输入元组的Generic实例 split: Split.Aux[All, N, CRep, ORep], // 将输入HList按N长度拆分为CommonFields和T#TheType对应部分 trGen: Generic.Aux[t.TheType, ORep] // T#TheType的Generic实例,调整到split之后 ): Wrapper[T] = { val all = iGen.to(in) val (cFields, tFields) = split(all) val com = cGen.from(cFields) val tr = trGen.from(tFields) Wrapper(com, tr) } } def construct[T <: TheTrait] = new Constructor[T]
此时报错变为No implicit arguments of type: Generic.Aux[B.Obj.TheType, String :: Int :: HNil]。
如果手动创建所需的Generic实例并将其声明为隐式值,代码确实可以正常编译:
implicit val objTypeGen: Generic.Aux[Obj.TheType, String :: Int :: HNil] = Generic.instance[Obj.TheType, String :: Int :: HNil]( t => t.name :: t.other :: HNil, { case n :: o :: HNil => Obj.Source(n, o) } )
但这种实现方式完全丧失了最初想要实现的易用性,希望找到无需手动传入TheTrait实例、也无需手动定义Generic实例的实现方案。
内容的提问来源于stack exchange,提问作者anqit

