You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Shapeless是否支持特质类型成员的Generic自动派生?

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 21:48:10