Scala3 GADT中上下文边界无法自动推断的解决方法
参考如下Scala 3代码,其中Obj[_]指代任意类型类,Context为广义代数数据类型(GADT):
enum Context[T: Obj]: case Empty[U <: Unit: Obj]() extends Context[U] case Variable[A: Obj](x: A) extends Context[A] case Product[A: Obj, B: Obj](ctx1: Context[A], ctx2: Context[B])(using Obj[(A,B)]) extends Context[(A,B)] def lookup[A: Obj, B: Obj](ctx: Context[A], x: B) = ctx match case Product(ctx1, ctx2): Context[(a1,b1)] => // 编译报错:No implicit argument of type Obj[a1] was found for an implicit parameter of method lookup. lookup[a1,B](ctx1, x) // 其余分支省略
上述代码的编译异常来自:Scala编译器无法在Product的匹配分支中推断类型a1满足Obj上下文边界,即使Product样例声明时已经明确要求A: Obj约束。
不需要在每次调用时手动显式传入隐式参数,核心解决思路是让GADT实例在构造时就捕获对应类型的类型类实例,存储为自身的成员字段,模式匹配时自动带入作用域,有两种落地方式:
方案1:修改枚举样例定义,将上下文边界对应的实例存为可访问的val成员
上下文边界T: Obj本质是为构造方法自动添加一个using Obj[T]的隐式参数,但Scala 3的enum默认不会将这类参数存为样例的公开成员,匹配时自然无法提取到对应的实例。只需要在声明时给隐式参数加上val修饰,标记为枚举样例的公开成员即可:
enum Context[T]: case Empty[U <: Unit](using val obj: Obj[U])() extends Context[U] case Variable[A](x: A)(using val obj: Obj[A]) extends Context[A] case Product[A, B](ctx1: Context[A], ctx2: Context[B])( using val objA: Obj[A], val objB: Obj[B], val objPair: Obj[(A, B)] ) extends Context[(A, B)]
修改后,模式匹配解构Product样例时,存储的objA、objB、objPair会自动被纳入当前分支的given作用域,lookup方法无需任何额外修改即可正常编译,所有类型类实例在GADT实例构造时就会自动被捕获,不需要手动传入。
方案2:为枚举根类型添加统一的类型类实例提取方法
如果不想改动现有枚举样例的声明,可以直接在Context枚举内部定义一个方法,直接获取当前实例对应的Obj[T]证据——因为枚举声明时已经要求T: Obj,构造实例时必然存在对应的given实例:
enum Context[T: Obj]: case Empty[U <: Unit: Obj]() extends Context[U] case Variable[A: Obj](x: A) extends Context[A] case Product[A: Obj, B: Obj](ctx1: Context[A], ctx2: Context[B])(using Obj[(A,B)]) extends Context[(A,B)] // 新增:获取当前实例对应的类型类实例 def instance: Obj[T] = summon[Obj[T]]
在匹配分支中,只需要将子节点的实例显式声明为given即可,不需要手动构造或外部传入实例:
def lookup[A: Obj, B: Obj](ctx: Context[A], x: B) = ctx match case p @ Product(ctx1, ctx2): Context[(a1,b1)] => // 从ctx1中提取对应的Obj[a1]实例,放入given作用域 given Obj[a1] = ctx1.instance given Obj[b1] = ctx2.instance lookup[a1,B](ctx1, x) // 其余分支逻辑
注意:不要尝试在匹配分支中直接写
summon[Obj[a1]]获取实例,此时编译器还未完成GADT类型细化对应的隐式作用域填充,会直接报错,必须从已构造的GADT实例成员中提取提前存储好的证据。
内容的提问来源于stack exchange,提问作者SvenK

