Search code examples
scalaabstract-type

How to compare Ordered abstract type in Scala trait?


Given the code below the method foo should compare operator-wise a given parameter bar with the lowerBound and upperBound all being of the same abstract type Bar.

trait Foo {
  type Bar <: Ordered[Bar]
  val lowerBound: Bar
  val upperBound: Bar
  def foo(bar: Bar) = bar >= lowerBound && bar <= upperBound
}

This way the trait Foo can be defined. The problems start with the below concrete class FooImpl.

class FooImpl extends Foo {
  type Bar = Int
  val lowerBound = 0
  val upperBound = 5
}

I understand that scala.Int isn't implementing what scala.runtime.RichInt does, effectively scala.math.Ordered[Int]. Defining type Bar as RichInt instead neither works as it does not conform to scala.math.Ordered[RichInt]. My third attempt to define type Bar as Ordered[Ord] where Ord is declared as type Ord and defining it in FooImpl as Int also did not work.

How would a possibly close solution look like?


Solution

  • There may be a more elegant solution, but you can achieve this by moving the restriction on the type to the method, rather than the type declaration:

    trait Foo {
      type Bar
      val lowerBound: Bar
      val upperBound: Bar
      def foo(bar: Bar)(implicit ev: Bar => Ordered[Bar]) = {
        bar >= lowerBound && bar <= upperBound
      }
    }
    

    Then your FooImpl works as you have it:

    class FooImpl extends Foo {
      type Bar = Int
      val lowerBound = 0
      val upperBound = 5
    }
    

    From the REPL:

    scala> new FooImpl()
    res0: FooImpl = FooImpl@2dbbec72
    
    scala> res0.foo(3)
    res1: Boolean = true
    
    scala> res0.foo(7)
    res2: Boolean = false
    

    The disadvantage here is that the trait can be extended with unordered types (although foo can't be called in that case):

    class A // not Ordered
    
    class BrokenFoo extends Foo {
      type Bar = A
      val lowerBound = new A
      val upperBound = new A
    } // compiles
    
    new BrokenFoo().foo(new A) // doesn't compile
    

    Alternatively, you can keep the requirement at the class level (and therefore stop anyone creating a BrokenFoo) as follows, but FooImpl has to change slightly:

    trait Foo {
      type Bar
      implicit val baz: Bar => Ordered[Bar]
      val lowerBound: Bar
      val upperBound: Bar
      def foo(bar: Bar) = { bar >= lowerBound && bar <= upperBound }
    }
    
    class FooImpl extends Foo {
      type Bar = Int
      val baz = implicitly[Bar => Ordered[Bar]]
      val lowerBound = 0
      val upperBound = 5
    }
    

    This problem feels like view or context bounds should be applicable, but unfortunately it doesn't seem like you can use them either in type declarations or in generic type parameters on traits.