测试 Scala 类型 class 中的成员资格

test for membership in Scala type class

有没有办法测试一个类型是否是一个类型class的成员?例如:

trait Foo[A]

trait Marshaller[Node] {
  def isFoo(n: Node): Boolean
}

class MyMarshaller[Node] extends Marshaller[Node] {
  override def isFoo(n: Node): Boolean = ???
}

显然存在成员资格时执行代码的解决方案:

def isFoo(a: A)(implicit ev: Foo[A]) = // do something Foo-like with `a`

在我的用例中,我重写了 isFoo 方法,所以我也无法更改它的签名。


现实世界的问题

Sangria is a library for creating GraphQL services in Scala. It has a marshalling subsystem 以有效类型 class、InputUnmarshaller[Node] 的形式交织其中。在代码中可以看到由上下文限定的类型参数:In: InputUnmarshaller.

概念是消耗输入值并生成输出数据集 a Value production, each element of which needs to be marshalled. The Node type can be restricted to, for example, io.circe.Json values, if one is using Circe for the marshalling

还有a Scala marshaller, which is quite dumb in that it only handles Map types as being map-like。目标是扩展它以支持案例 classes,例如,通过 Shapeless 和类似地图的类型 class.

(1) 尝试引入类型 class IsFoo

trait Foo[A] 

trait IsFoo[A] {
  def value(): Boolean
}
trait LowPriorityIsFoo {
  implicit def noFoo[A]: IsFoo[A] = () => false
}
object IsFoo extends LowPriorityIsFoo {
  implicit def existsFoo[A](implicit foo: Foo[A]): IsFoo[A] = () => true
}

def isFoo[A](implicit isFooInst: IsFoo[A]): Boolean = isFooInst.value()

测试:

implicit val intFoo: Foo[Int] = null

isFoo[Int] // true
isFoo[String] // false

实际上,我猜我的 isFoo 只是 @LuisMiguelMejíaSuárez 的一个更复杂的变体 def isFoo[A](implicit ev: Foo[A] = null): Boolean = Option(ev).isDefined


(2) 您想通过继承/子类型多态性定义 Marshaller 的行为。在 JVM 语言中,它是动态调度的(最近,在运行时)。现在您想将它与隐式/类型 classes (Foo) / 静态调度的临时多态性(早期,在编译时)混合使用。您必须使用一些运行时工具,例如运行时反射(使用 TypeTags 将有关 Node 的编译时信息保存到运行时)、运行时编译。

import scala.reflect.runtime.universe.{TypeTag, typeOf, Quasiquote}
import scala.reflect.runtime.currentMirror
import scala.tools.reflect.ToolBox
val tb = currentMirror.mkToolBox()

trait Foo[A]

trait Marshaller[Node] {
  def isFoo(n: Node): Boolean
}

class MyMarshaller[Node: TypeTag] extends Marshaller[Node] {
// override def isFoo(n: Node): Boolean =
//   tb.inferImplicitValue(
//     tb.typecheck(tq"Foo[${typeOf[Node]}]", mode = tb.TYPEmode
//   ).tpe).nonEmpty
  override def isFoo(n: Node): Boolean = 
    util.Try(tb.compile(q"implicitly[Foo[${typeOf[Node]}]]")).isSuccess
}

implicit val intFoo: Foo[Int] = null
new MyMarshaller[Int].isFoo(1) //true
new MyMarshaller[String].isFoo("a") //false

Scala resolving Class/Type at runtime + type class constraint

In scala 2 or 3, is it possible to debug implicit resolution process in runtime?


(3) 如果你只想检查 NodeMap[String, _] 那么运行时反射就足够了

import scala.reflect.runtime.universe.{TypeTag, typeOf}

trait Marshaller[Node] {
  def isFoo(n: Node): Boolean
}

class MyMarshaller[Node: TypeTag] extends Marshaller[Node] {
  override def isFoo(n: Node): Boolean = typeOf[Node] <:< typeOf[Map[String, _]]
}

new MyMarshaller[Map[String, _]].isFoo(Map()) //true
new MyMarshaller[Int].isFoo(1) //false

另见 Typeable

import shapeless.Typeable

trait Marshaller[Node] {
  def isFoo(n: Node): Boolean
}

class MyMarshaller[Node] extends Marshaller[Node] {
  override def isFoo(n: Node): Boolean = Typeable[Map[String, _]].cast(n).isDefined
}

new MyMarshaller[Map[String, _]].isFoo(Map()) //true
new MyMarshaller[Int].isFoo(1) //false

(4) 在 Scala 3 中你可以使用 pattern matching by implicits

import scala.compiletime.summonFrom

trait Foo[A]

trait Marshaller[Node] {
  def isFoo(n: Node): Boolean
}

class MyMarshaller[Node] extends Marshaller[Node] {
  override inline def isFoo(n: Node): Boolean = summonFrom {
    case given Foo[Node] => true
    case _ => false
  }
}

given Foo[Int] with {}

new MyMarshaller[Int].isFoo(1) //true
new MyMarshaller[String].isFoo("a") //false

(5) 实际上,我想最简单的方法是将隐式参数从方法移动到 class

trait Foo[A]

trait IsFoo[A] {
  def value(): Boolean
}
trait LowPriorityIsFoo {
  implicit def noFoo[A]: IsFoo[A] = () => false
}
object IsFoo extends LowPriorityIsFoo {
  implicit def existsFoo[A: Foo]: IsFoo[A] = () => true
}

trait Marshaller[Node] {
  def isFoo(n: Node): Boolean
}

class MyMarshaller[Node: IsFoo] extends Marshaller[Node] {
//                       ^^^^^  HERE
  override def isFoo(n: Node): Boolean = implicitly[IsFoo[Node]].value()
}

implicit val intFoo: Foo[Int] = null

new MyMarshaller[Int].isFoo(1) //true
new MyMarshaller[String].isFoo("a") //false

(6) @LuisMiguelMejíaSuárez 在这种情况下也可以使用默认隐式的想法

trait Foo[A]

trait Marshaller[Node] {
  def isFoo(n: Node): Boolean
}

class MyMarshaller[Node](implicit ev: Foo[Node] = null) extends Marshaller[Node] {
  override def isFoo(n: Node): Boolean = Option(ev).isDefined
}

implicit val intFoo: Foo[Int] = new Foo[Int] {}

new MyMarshaller[Int].isFoo(1) //true
new MyMarshaller[String].isFoo("a") //false