38

What does [B >: A] mean in Scala? And what are the effects?

Example reference: http://www.scala-lang.org/node/129

class Stack[+A] {
    def push[B >: A](elem: B): Stack[B] = new Stack[B] {
        override def top: B = elem
        override def pop: Stack[B] = Stack.this
        override def toString() = elem.toString() + " " + Stack.this.toString()
    }
    def top: A = error("no element on stack")
    def pop: Stack[A] = error("no element on stack")
    override def toString() = ""
}

object VariancesTest extends Application {
    var s: Stack[Any] = new Stack().push("hello");
    s = s.push(new Object())
    s = s.push(7)
    println(s)
}
Maroun
  • 94,125
  • 30
  • 188
  • 241
Jay Taylor
  • 13,185
  • 11
  • 60
  • 85
  • Judging by the context, it looks like "allow B to represent a class that is also representable by A" but that's just a guess. – corsiKa Oct 13 '11 at 19:27
  • 1
    Try this on the REPL: `val s1 = new Stack().push("hello"); val s2 = s1.push(new Object()); val s3 = s2.push(7)` -- how do s1, s2, and s3 differ? (This is hidden behind the `Stack[Any]` type given to s; remember that Any is the topmost type in Scala, not Object.) –  Oct 13 '11 at 19:36
  • Duplicate of http://stackoverflow.com/questions/4869988/identify-and-describe-scalas-generic-type-constraints – greenoldman Oct 14 '11 at 05:10
  • @macias Too bad that totally did not come up when I searched for similar questions before posting. I vote to keep these great answers.. – Jay Taylor Oct 14 '11 at 19:08
  • @pyrony, marking as duplicate does not delete answers, but building a web of duplicates actually helps seeing big picture of the problem (and ALL answers). I am not surprised you didn't hit that post, the subject is totally different :-). – greenoldman Oct 15 '11 at 12:18

3 Answers3

47

[B >: A] is a lower type bound. It means that B is constrained to be a supertype of A.

Similarly [B <: A] is an upper type bound, meaning that B is constrained to be a subtype of A.

In the example you've shown, you're allowed to push an element of type B onto a stack containing A elements, but the result is a stack of B elements.

The page where you saw this actually has a link to another page about lower type bounds, which includes an example showing the effect.

Don Roby
  • 40,677
  • 6
  • 91
  • 113
26

X <: Y means type parameter X must be a subtype of type Y. X >: Y means the opposite, X must be a super type of Y (in both cases, X = Y is ok). This notation can be contrary intuition, one may think a dog is more than an animal (more precise of in programming terms, more services), but for the very reason it is more precise, there are less dogs than there are animals, the type Animal contains more values than the type Dog, it contains all dogs, and all ostriches too. So Animal >: Dog.

As for the reason why push has this signature, I'm not sure I can explain it better than the page the example comes from, but let me try.

It starts with variance. The + in class Stack[+A] means that Stack is covariant in A. if X is a subtype of Y, Stack[X] will be a subtype of Stack[Y]. A stack of dogs is also a stack of animals. For the mathematically inclined, if one sees Stack as a function from type to type (X is a type, if you pass it to Stack, you get Stack[X], which is another type), being covariant means that it is an increasing function (with <:, the subtyping relation being the orders on types).

This seems right, but this is not such an easy question. It would not be so, with a push routine that modifies it, adding a new element, that is

def push(a: A): Unit

(the example is different, push returns a new stack, leaving this unchanged). Of course, a Stack[Dog] should only accept dogs to be pushed into it. Otherwise, it would no longer be a stack of dogs. But if we accept it to be treated as a stack of animals, we could do

val dogs : Stack[Dog] = new Stack[Dog]
val animals : Stack[Animal] = dogs // if we say stack is covariant
animals.push(ostrich) // allowed, we can push anything in a stack of any. 
val topDog: Dog = dogs.top  // ostrich!

Clearly, treating this stack as covariant is unsound. When the stack is seen as a Stack[Animal], an operation is allowed that would not be on Stack[Dog]. What was done here with push can be done with any routine that takes A as its argument. If a generic class is marked as covariant, with C[+A], then A cannot be the type of any argument of any (public) routine of C, and the compiler will enforce that.

But the stack in the exemple is different. We would have a def push(a: A): Stack[A]. If one calls push, one gets a new stack, and the original stack is left unchanged, it is still a proper Stack[Dog], whatever may have been pushed. If we do

val newStack = dogs.push(ostrich)

dogs is still the same and still a Stack[Dog]. Obviously newStack is not. Nor is it a Stack[Ostrich], because it also contains the dogs that were (and still are) in the original stack. But it would be a proper Stack[Animal]. If one pushes a cat, it would be more precise to say it is a Stack[Mammal] (while being a stack of animals too). If one pushes 12, it will be only a Stack[Any], the only common supertype of Dog and Integer. The problem is that the compiler has no way to know that this call is safe, and will not allow the a: A argument in def push(a: A): Stack[A] if Stack is marked covariant. If it stopped there, a covariant stack would be useless because there would be no way to put values in it.

The signature solves the problem:

def push[B >: A](elem: B): Stack[B]

If B is an ancestor of A, when adding a B, one gets a Stack[B]. So adding a Mammal to a Stack[Dog] gives a Stack[Mammal], adding an animal gives a Stack[Animal], which is fine. Adding a Dog is ok too, A >: A is true.

This is good, but seems too restrictive. What if the added item's type is not an ancestor of A? For instance, what if it is a descendant e.g dogs.push(goldenRetriever). One cannnot take B = GoldenRetriever, one has not GoldenRetriever >: Dog, but the opposite. Yet, one can take B = Dog all right. It the parameter elem is expected to be of type Dog, we can pass of course pass a GoldenRetriever. One gets a stack of B, still a stack of dogs. And it is right that B = GoldenRetriever was not allowed. The result would have been typed as Stack[GoldenRetriever], which would be wrong because the stack may have contained irish setters too.

What about ostrishes? Well Ostrich is neither an supertype, nor a subtype of Dog. But just as one can add a goldenRetriever because it is a dog, and it is possible to add a dog, an ostrich is an animal, and it is possible to add an animal. So taking B = Animal >: Dog works, and so a when pushing an ostrich, one gets a Stack[Animal].

Making the stack covariant force this signature, more complex than the naïve push(a: A) : Stack[A]. But we gain a routine that is completely flexible, anything can be added, not just an A, and yet, types the result as precisely as can be. And the actual implementation, except for the types declarations, is the same it would have been with push(a: A).

Didier Dupont
  • 29,398
  • 7
  • 71
  • 90
  • I didn't understand this `Stack is covariant in A`. Can you please explain – Aman Jain Sep 25 '18 at 03:02
  • 1
    Not sure I would do much better than before. Saying that Stack is covariant in A is the same as saying if X is a subtype of Y, Stack[X] will be a subtype of Stack[Y], which is also to say that a Stack[Dog] is a valid Stack[Animal]. See https://docs.scala-lang.org/tour/variances.html – Didier Dupont Oct 07 '18 at 20:28
3

As a great overview, see the git page of @retronym

Peter Schmitz
  • 5,824
  • 4
  • 26
  • 48
  • Really worth a read. There are examples that bridge upper/lower bound and view bound. – lcn Feb 19 '14 at 18:34