Shapeless编译报错:无法找到Coproduct映射隐式值,求解决
咱们先把这个编译错误的核心说透:这是Scala隐式解析的作用域规则在搞鬼,结合shapeless的Mapper推导逻辑,就导致了直接调用map可行,但transform方法找不到隐式的情况。
为什么直接result.searchCriteria.map(transformer)能跑通?
当你在CirceShapeApp内部直接调用coproduct的map方法时,编译器的隐式查找范围是当前作用域——而transformer这个Poly1的两个implicit case定义正好在这个作用域里。shapeless的Mapper可以自动基于这些case推导出对应的隐式实例,所以一切正常。
为什么SearchCriteria.transform方法卡壳了?
SearchCriteria是个独立的类,它的transform方法只声明了需要一个Mapper.Aux[...]隐式参数,但编译器不知道这个Mapper要基于transformer来构建啊!
当你调用result.transform时,编译器会按以下顺序找隐式:
- 当前调用点的作用域(也就是
CirceShapeApp内部) SearchCriteria的伴生对象Mappertrait的伴生对象- 与
Mapper.Aux类型相关的隐式作用域
但transformer的那些implicit def是藏在CirceShapeApp内部的transformer object里的,编译器没办法自动把transformer和Mapper的推导关联起来——除非你明确告诉它:“嘿,用这个transformer来构建Mapper!”
几种修复方案
方案1:把transformer作为隐式参数传入transform方法
修改SearchCriteria的方法,让它同时接受transformer的隐式引用,这样编译器就知道要用这个transformer来推导Mapper:
class SearchCriteria(val searchCriteria: SearchBy) { // 这里添加transformer的隐式参数,让编译器明确依赖 def transform(implicit transformerInstance: transformer.type, mapper: Mapper.Aux[transformerInstance.type, SearchBy, JsonPayload] ) = { searchCriteria map transformerInstance } }
调用result.transform时,因为transformer在CirceShapeApp作用域里是可见的,会被自动传入,编译器就能顺利推导出Mapper实例。
方案2:把transformer移到全局/伴生对象作用域
如果这个transformer是通用的,可以把它移到SearchCriteria的伴生对象或者顶级作用域,这样transform方法的隐式查找路径就能覆盖到它:
class SearchCriteria(val searchCriteria: SearchBy) { def transform(implicit mapper: Mapper.Aux[transformer.type, SearchBy, JsonPayload]) = { searchCriteria map transformer } } // 把transformer放到SearchCriteria的伴生对象里 object SearchCriteria { object transformer extends Poly1 { implicit def casePersonalDetails = at[PersonalDetails](v => s"""{"bd": "${v.birthDate}", "ln":"${v.lastName}"}""") implicit def casePersonalAddress = at[PersonalAddress](v => s"""{"pc": "${v.postalCode}", "hn":"${v.houseNumber}"}""") } }
然后在CirceShapeApp里导入SearchCriteria.transformer,就能正常调用了。
方案3:简化写法,直接在方法内指定隐式推导
还有更偷懒的方式,不需要额外声明参数,直接在方法内部让编译器基于当前作用域的transformer推导Mapper:
class SearchCriteria(val searchCriteria: SearchBy) { def transform = { // 显式引用shapeless的Mapper,编译器会自动从当前作用域找transformer的case searchCriteria.map(transformer)(shapeless.ops.coproduct.Mapper[transformer.type, SearchBy]) } }
修正后的完整代码(方案1示例)
import io.circe.generic.auto._ import io.circe.shapes._ import io.circe.parser._ import shapeless._ import shapeless.ops.coproduct.Mapper object CirceShapeApp extends App { val jsonPersonalDetails = """{"searchCriteria": {"birthDate":"01-01-1980", "lastName":"John"}}""".stripMargin val jsonPersonalAddress = """{"searchCriteria": {"postalCode":"10776", "houseNumber":"34"}}""".stripMargin class SearchCriteria(val searchCriteria: SearchBy) { def transform(implicit t: transformer.type, mapper: Mapper.Aux[t.type, SearchBy, JsonPayload] ) = { searchCriteria map t } } case class PersonalDetails(birthDate: String, lastName: String) case class PersonalAddress(postalCode: String, houseNumber: String) type SearchBy = PersonalDetails :+: PersonalAddress :+: CNil type JsonPayload = String :+: CNil object transformer extends Poly1 { implicit def casePersonalDetails = at[PersonalDetails](v => s"""{"bd": "${v.birthDate}", "ln":"${v.lastName}"}""") implicit def casePersonalAddress = at[PersonalAddress](v => s"""{"pc": "${v.postalCode}", "hn":"${v.houseNumber}"}""") } val result = decode[SearchCriteria](jsonPersonalDetails).right.get println(result.searchCriteria.map(transformer)) // WORKS println(result.transform) // NOW WORKS }
内容的提问来源于stack exchange,提问作者Octavian R.

