当类型类中有类型类时,使用类型类语法的正确方法是什么?

What is the correct way to use typeclass syntax when you have a typeclass within a typeclass?

我有一种情况,我正在使用一个类型class-within-a-typeclass 来重载原始类型class 的方法。示例如下:

abstract class IsArray[A, T: Numeric] {
  def getSingleElem(self: A, idx: Int): T
  def getRef[R](self: A, ref: R)(implicit refTc: RefTC[R]): refTc.Out = refTc.getRef(self, ref)

  trait RefTC[R] {
    type Out
    def getRef(self: A, ref: R): Out
  }
  object RefTC {
    implicit val numsTcForSingleInt = new RefTC[Int] {
      type Out = T
      def getRef(self: A, ref: Int): Out = getSingleElem(self, ref)
    }
    implicit val numsTcForListInt = new RefTC[List[Int]] {
      type Out = List[T]
      def getRef(self: A, ref: List[Int]): Out = ref.map(getSingleElem(self, _))
    }
  }
}

一切正常。我遇到困难的地方是为类型 class 创建一个 'syntax' 对象,因此可以直接从实现类型 class 的值调用这些方法。我的第一次尝试看起来像这样并进行类型检查 OK:

object IsArraySyntax {
  implicit class IsArrayOps1[A, T: Numeric](self: A)(implicit isArTc: IsArray[A, T]) {
    def getSingleElem(idx: Int): T = isArTc.getSingleElem(self, idx)
    def getRef[R](ref: R)(implicit refTc: isArTc.RefTC[R]): refTc.Out = refTc.getRef(self, ref)
  }
}

但是,我在使用它时遇到了一些奇怪的错误(例如 ),我想知道我写这篇文章的方式是否有问题。 isArTc 类型class 和refTc 类型class 之间实际上存在依赖关系,如果isArTc 类型class 是方法而不是 IsArrayOps1 class,像这样:

object IsArraySyntax {
  implicit class IsArrayOps2[A, T: Numeric](self: A) {
    def getSingleElem(idx: Int)(implicit isArTc: IsArray[A, T]): T = isArTc.getSingleElem(self, idx)
    def getRef[R](ref: R)(implicit isArTc: IsArray[A, T], refTc: isArTc.RefTC[R]): refTc.Out = refTc.getRef(self, ref)
  }
}

这不是类型检查,可能需要 Aux 模式才能工作。

但我更想知道这里的最佳做法是什么?将 isArTc typeclass 作为 implicit class 的 属性 而不是每个方法似乎减少了样板并简化了 typeclass 依赖性,但我没有看到它被使用了,我想知道它是否因为其他原因而不受欢迎? refTc: isArTc.RefTC[R] 属于另一种类型 class 的类型 class 的正确语法是 refTc: isArTc.RefTC[R] 还是应该更像 refTc: IsArray#RefTC[R]?

嵌套类型class很少使用,但原则上可以使用。

另一个嵌套类型的例子 class 是

This does not type check and presumably requires the Aux pattern to be made to work.

不,Aux 模式无济于事。 Aux 模式有助于解决类型 parameters/type 成员中的依赖关系,但无法解决 IsArrayOps2 中前缀中的依赖关系。这种依赖性在 Scala 2 中是无法表达的。

实际上,将隐式参数拆分为 class-level 和 method-level 两个参数(如 IsArrayOps1)是对抗这种依赖性的正确方法。

should it be something more like refTc: IsArray#RefTC[R]?

不,类型投影不能很好地使用隐式解析

https://typelevel.org/blog/2015/07/23/type-projection.html

您可以使用类型投影检查您的语法是否有效。

I've been getting some strange errors whilst using this (eg java.lang.NoSuchFieldError when using ScalaTest) and I'm wondering if the way I'm writing this is to blame.

您的类型 classes 和语法 #1 似乎有效

case class MyClass(is: List[Int])
object MyClass {
  implicit val mcIsIntArray: IsArray[MyClass, Int] = new IsArray[MyClass, Int] {
    override def getSingleElem(self: MyClass, idx: Int): Int = self.is(idx)
  }
  implicit val mcIsDoubleArray: IsArray[MyClass, Double] = new IsArray[MyClass, Double] {
    override def getSingleElem(self: MyClass, idx: Int): Double = self.is(idx)
  }
}

val ia = implicitly[IsArray[MyClass, Int]]
implicitly[ia.RefTC[Int] { type Out = Int}]
implicitly[ia.RefTC[List[Int]] { type Out = List[Int]}]
val ia1 = implicitly[IsArray[MyClass, Double]]
implicitly[ia1.RefTC[Int] { type Out = Double}]
implicitly[ia1.RefTC[List[Int]] { type Out = List[Double]}]

implicitly[IsArray[MyClass, Int]].getSingleElem(MyClass(List(1, 2, 3)), 1) // 2
implicitly[IsArray[MyClass, Int]].getRef(MyClass(List(1, 2, 3)), 1) // 2
implicitly[IsArray[MyClass, Int]].getRef(MyClass(List(1, 2, 3)), List(1, 0)) // List(2, 1)
implicitly[IsArray[MyClass, Double]].getSingleElem(MyClass(List(1, 2, 3)), 1) // 2.0
implicitly[IsArray[MyClass, Double]].getRef(MyClass(List(1, 2, 3)), 1) // 2.0 
implicitly[IsArray[MyClass, Double]].getRef(MyClass(List(1, 2, 3)), List(1, 0)) // List(2.0, 1.0)

import IsArraySyntax._
{
  import Numeric.IntIsIntegral // to avoid ambiguity
  MyClass(List(1, 2, 3)).getSingleElem(1): Int
  MyClass(List(1, 2, 3)).getRef(1): Int
  MyClass(List(1, 2, 3)).getRef(List(1, 0)): List[Int]
}

{
  import Numeric.DoubleIsFractional // to avoid ambiguity
  MyClass(List(1, 2, 3)).getSingleElem(1): Double
  MyClass(List(1, 2, 3)).getRef(1): Double
  MyClass(List(1, 2, 3)).getRef(List(1, 0)): List[Double]
}

我将隐式 Numeric.IntIsIntegralNumeric.DoubleIsFractional 导入到相应的范围以避免歧义,如

顺便说一下,您可以用单一类型表达相同的逻辑 class 添加更多类型参数和条件隐式 (listIsArray)

abstract class IsArray[A, T: Numeric, R, Out] {
  def getRef(self: A, ref: R): Out
}
trait LowPriorityIsArray {
  implicit def listIsArray[A, T: Numeric, R, Out](implicit
    singleIsArray: IsArray[A, T, R, Out]
  ): IsArray[A, T, List[R], List[Out]] = new IsArray[A, T, List[R], List[Out]] {
    override def getRef(self: A, ref: List[R]): List[Out] =
      ref.map(singleIsArray.getRef(self, _))
  }
}
object IsArray extends LowPriorityIsArray {
  implicit val mcIsIntArray: IsArray[MyClass, Int, Int, Int] = new IsArray[MyClass, Int, Int, Int] {
    override def getRef(self: MyClass, idx: Int): Int = self.is(idx)
  }
  implicit val mcIsDoubleArray: IsArray[MyClass, Double, Int, Double] = new IsArray[MyClass, Double, Int, Double] {
    override def getRef(self: MyClass, idx: Int): Double = self.is(idx)
  }
}

object IsArraySyntax {
  implicit class IsArrayOps3[A, T: Numeric, R, Out](self: A) {
    def getSingleElem(idx: Int)(implicit 
      isAr: IsArray[A, T, R, Out], 
      ev: Int <:< R
    ): Out = isAr.getRef(self, idx)
    def getRef(ref: R)(implicit isAr: IsArray[A, T, R, Out]): Out =
      isAr.getRef(self, ref)
  }
}
    
case class MyClass(is: List[Int])

import IsArraySyntax._
{
  import Numeric.IntIsIntegral
  MyClass(List(1, 2, 3)).getSingleElem(1): Int
  MyClass(List(1, 2, 3)).getRef(1): Int
  MyClass(List(1, 2, 3)).getRef(List(1, 0)): List[Int]
}

{
  import Numeric.DoubleIsFractional
  MyClass(List(1, 2, 3)).getSingleElem(1): Double
  MyClass(List(1, 2, 3)).getRef(1): Double
  MyClass(List(1, 2, 3)).getRef(List(1, 0)): List[Double]
}

注意语法

object IsArraySyntax {
  implicit class IsArrayOps4[A, T: Numeric, R, Out](self: A)(implicit
    isAr: IsArray[A, T, R, Out]
  ) {
    def getSingleElem(idx: Int)(implicit ev: Int <:< R): Out =
      isAr.getRef(self, idx)
    def getRef(ref: R): Out = isAr.getRef(self, ref)
  }
}

不会工作。

或者您可以使 Out 成为类型成员而不是类型参数。

嗯,我刚刚注意到我实际上没有在我的类型 class 中使用 T,所以这可能不是您想要的。您可以尝试单个 higher-kinded 类型 class

abstract class IsArray[A, T: Numeric, Col[_], R <: Col[T], Out] {
  def getRef(self: A, ref: R): Out
}

或者只是

abstract class IsArray[A, T: Numeric, Col[_], Out] {
  def getRef(self: A, ref: Col[T]): Out
}

在我们的实例中 Col 可以是 IdList