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

Viewed 102

I have a situation where I am using a typeclass-within-a-typeclass in order to overload methods of the original typeclass. Example below:

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, _))
    }
  }
}

This all works fine. Where I am having difficulties is in creating a 'syntax' object for the typeclass, so the methods can be called directly from values that implement the typeclass. My first attempt looks like this and type-checks 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)
  }
}

However, 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. There is effectively a dependency between the isArTc typeclass and the refTc typeclass, which becomes an explicit dependency if the isArTc typeclass is a parameter of the method rather than than of the IsArrayOps1 class, likeso:

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)
  }
}

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

But I was more wondering what is best practice here? Having the isArTc typeclass as a property of the implicit class rather than each method seems to reduce boilerplate and simplify typeclass dependencies, but I've not seen it used and I was wondering if it was undesirable for some other reason? And is refTc: isArTc.RefTC[R] the correct syntax for a typeclass belonging to another typeclass, or should it be something more like refTc: IsArray#RefTC[R]?

1 Answers

Nested type classes are used rarer but in principle they can be used.

Another example of nested type class is How to avoid calling asInstanceOf in Scala

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

No, Aux pattern will not help. Aux pattern helps with dependencies in type parameters/type members but can't help with depedencies in prefixes like in IsArrayOps2. Such kind of dependency just can't be expressed in Scala 2.

Actually, splitting implicit parameters into class-level and method-level ones (like in IsArrayOps1) is correct way to fight with such kind of dependency.

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

No, type projections don't play well with implicit resolution

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

You can check that with type projections your syntax will not work.

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.

Your type classes and syntax #1 seem to work

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]
}

I imported implicits Numeric.IntIsIntegral, Numeric.DoubleIsFractional to corresponding scopes to avoid ambiguity like in Implicit view not working - is my implicit def to blame?

By the way, you can express the same logic with a single type class adding more type parameters and conditional implicits (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]
}

Notice that syntax

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)
  }
}

will not work.

Or you can make Out a type member rather than type parameter.

Well, I've just noticed that I actually didn't use T in my type class, so probably that's not what you wanted. You can try a single higher-kinded type class

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

or just

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

Here in our instances Col can be Id or List.

Related