scalagenericstypesf-bounded-polymorphism

Scala Type mismatch when a generic type operates on the same generic type


I have an generic case class Route that takes in a List of subclasses of Location. However in the following method I get a type mismatch in the call to distance expected: head.T, actual: T

case class Route[T <: Location](route: List[T]) {
  def measureDistance: Double = {
    def measure(head: T, tail: List[T], acc: Double = 0.0): Double = tail match {
      case Nil => acc
      case h :: t => measure(h, t, head.distance(h) + acc)
    }
    if (route.isEmpty) 0.0
    else measure(route.head, route.tail)
  }
}

The basic abstract Location class is as follows

abstract class Location(val name: String) {

type T <: Location

def distance(that: T): Double
}

As head and h both come from the same list route I can't understand why these are not the same type.


Solution

  • It looks as if F-bounded polymorphism is what you want in this case:

    abstract class Location[L <: Location[L]](val name: String) {
      def distance(that: L): Double
    }
    
    case class Route[T <: Location[T]](route: List[T]) {
      def measureDistance: Double = {
        def measure(head: T, tail: List[T], acc: Double = 0.0): Double = tail match {
          case Nil => acc
          case h :: t => measure(h, t, head.distance(h) + acc)
        }
        if (route.isEmpty) 0.0
        else measure(route.head, route.tail)
      }
    }
    

    However, you might also consider using a Metric-typeclass instead:

    trait Metric[L] {
      def dist(a: L, b: L): Double
    }
    
    case class Route[T: Metric](route: List[T]) {
      def measureDistance: Double = {
        def measure(head: T, tail: List[T], acc: Double = 0.0): Double = tail match {
          case Nil => acc
          case h :: t => measure(h, t, implicitly[Metric[T]].dist(head, h) + acc)
        }
        if (route.isEmpty) 0.0
        else measure(route.head, route.tail)
      }
    }
    

    The latter solution would be applicable to more types, for example to (Double, Double), even if they don't inherit from Location.

    Here is the typeclass solution again, but with slightly more polished Cats-style syntax that avoids implicitly:

    trait Metric[L] {
      def dist(a: L, b: L): Double
    }
    
    object Metric {
      def apply[T](implicit m: Metric[T]): Metric[T] = m
    }
    
    case class Route[T: Metric](route: List[T]) {
      def measureDistance: Double = {
        def measure(head: T, tail: List[T], acc: Double = 0.0): Double = tail match {
          case Nil => acc
          case h :: t => measure(h, t, Metric[T].dist(head, h) + acc)
        }
        if (route.isEmpty) 0.0
        else measure(route.head, route.tail)
      }
    }