How does Upper Type Bounds work in scala?

My Code:

object Main extends App {
  def foo[K >: Baz](a: K): Unit = ???

  foo(new Foo)
  foo(new Baz)
  foo(new Bax)
  foo("fi")
}
class Foo
class Baz extends Foo
class Bax extends Baz

My understanding of K >: Baz - K can be of type Baz or any super type of Baz.
But the code seems to compile with any type. I am pretty sure I have misunderstood something here?

It’s because of subtyping. If you fix K = Any then it’s obvious that the code will compile as everything is a subtype of Any.

You need to explicitly specify your desired top type (different than Any as everything is Any) and enforce it somehow. Here’s an example:

  class Top
  class Foo extends Top
  class Baz extends Foo
  class Bax extends Baz

  def main(args: Array[String]): Unit = {
    trait BazEv[K]
    object BazEv {
      implicit def bazIsBetweenTopAndFoo[K >: Foo <: Top]: BazEv[K] = null
      implicit def bazSubTypesAreBaz[K <: Baz]: BazEv[K] = null
    }
    def foo[K](a: K)(implicit ev: BazEv[K]): Unit = ???

    foo(new Foo)
    foo(new Baz)
    foo(new Bax)
    foo("fi") // only this line doesn't compile
  }

However it still doesn’t really make sense to do that, as you can ascribe any subtype of Top as Top, so the method that really makes sense is:

def foo(a: Top): Unit = ???
1 Like

As @tarsa said, this works because scala is free to pick any supertype of Baz of which there is always at least one, namely Any (actually AnyRef/Object in this case).

This shows it more clearly:

@ def foo[K >: Baz](a: K): Unit = println(a)
defined function foo

@ foo[AnyRef]("fi")
fi


@ foo[String]("fi")
cmd17.sc:1: type arguments [String] do not conform to method foo's type parameter bounds [K >: ammonite.$sess.cmd2.Baz]
val res17 = foo[String]("fi")
               ^
Compilation Failed

Where lower type bounds are often used is when the function automatically widens the return type based on the type parameter to the function.

One use case you have probably seen is for scala.Option#getOrElse which is defined as:

def getOrElse[B >: A](default: => B): B

Which you can see will infer B to be the lowest common supertype of A and the parameter default:

@ Option.empty[Baz].getOrElse(new Baz)
res22: Baz = ammonite.$sess.cmd18$Baz@42af2977

@ Option.empty[Baz].getOrElse(new Foo)
res23: Foo = ammonite.$sess.cmd17$Foo@17931cc0

@ Some(new Baz).getOrElse(new Foo)
res24: Foo = ammonite.$sess.cmd18$Baz@28806954

@ Some(new Baz).getOrElse(new Bax)
res25: Baz = ammonite.$sess.cmd18$Baz@1a9cfd5f

@ class Bay extends Foo
defined class Bay

@ Some(new Baz).getOrElse(new Bay)
res28: Foo = ammonite.$sess.cmd18$Baz@1835b24b

@ Some(new Baz).getOrElse("fi")
res29: Object = ammonite.$sess.cmd18$Baz@77b6d94c
2 Likes

thank you for the reply. On a different note, I see that you are using ammonite:) Any feedback on it?

I’ll jump in with an opinion: having picked up Ammonite a couple of months ago, I’m a fan. The power of the REPL aside, being able to do shell scripting in Scala is a revelation for me – it is so much easier than traditional scripting languages for me. I find myself actually looking forward to writing scripts, which is new and different.

(This is all from the viewpoint of already knowing Scala pretty well, mind. But if you do, it’s well worth checking out.)

3 Likes

Will definitely give it a try:)

I have only just started using it in the past few weeks and I really like it. Even the simple things like being able to import dependencies and the ability to edit your code if you make a mistake or want to modify it are really nice.

2 Likes