Search code examples
scalacollectionstypesspecializationfastutil

How to Run and Different Code Paths Based on Specialisation


In using a framework like FastUtils with Scala, how do you generate the appropriate code based on the effective specialization as the framework itself has specialised data structures? I.e., how you you programmatically figure out what is being specialized and execute the appropriate code? So how do you deal with path related typing in such cases.

For objects

class Container[@specialized T](var window: Int) {
  val data = new ObjectArrayList[T](window)
}

For char I want it to be:

class Container[@specialized T](var window: Int) {
  val data = new CharArrayList(window)
}

But this should be based on the specialization of T. If I am to put this differently the sudo code would be perhaps like

class Container[@specialized T](var window: Int) {
  val data = specialisationOf(T) match {
    case "Char" => new CharArrayList(window)
    case "Int" => new IntegerArrayList(window)
    ...
    ...
    ...
    case _ => new ObjectArrayList[T](window)
  }
}

Solution

  • As already explained in this question, you can encapsulate the specialized implementation in a typeclass. This would look more or less like the following code.

    import it.unimi.dsi.fastutil.ints.IntArrayList
    import it.unimi.dsi.fastutil.chars.CharArrayList
    import it.unimi.dsi.fastutil.objects.ObjectArrayList
    
    class Container[@specialized(Int,Char) T](window: Int)(implicit impl: ContainerImpl[T]) {
      impl.init(window)
    
      def add(element: T) = impl.add(element)
      override def toString = impl.toString
    }
    
    trait ContainerImpl[@specialized(Int,Char) T] {
      def init(window: Int): Unit
      def add(element: T): Unit
      def toString: String
    }
    
    object ContainerImpl extends LowerPriorityImplicits {
      implicit def intContainer = new ContainerImplInt
      implicit def charContainer = new ContainerImplChar
    }
    
    trait LowerPriorityImplicits {
      implicit def anyContainer[T] = new ContainerImplT[T]
    }
    
    final class ContainerImplInt extends ContainerImpl[Int] {
      var data: IntArrayList = _
      def init(window: Int) = data = new IntArrayList(window)
      def add(element: Int) = data.add(element)
      override def toString = data.toString
    }
    
    final class ContainerImplChar extends ContainerImpl[Char] {
      var data: CharArrayList = _
      def init(window: Int) = data = new CharArrayList(window)
      def add(element: Char) = data.add(element)
      override def toString = data.toString
    }
    
    final class ContainerImplT[T] extends ContainerImpl[T] {
      var data: ObjectArrayList[T] = _
      def init(window: Int) = data = new ObjectArrayList(window)
      def add(element: T) = data.add(element)
      override def toString = data.toString
    }
    

    Do note that although the implementation of add always looks the same, the method being called on data is a different overload every time. If you would write this in a more polymorphic way, the most specific add method would not be chosen, and your Int or Char will need to be boxed.