Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What does [B >: A] do in Scala?

Tags:

syntax

scala

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) } 
like image 788
Jay Taylor Avatar asked Oct 13 '11 19:10

Jay Taylor


People also ask

What is the meaning of => in Scala?

=> is syntactic sugar for creating instances of functions. Recall that every function in scala is an instance of a class. For example, the type Int => String , is equivalent to the type Function1[Int,String] i.e. a function that takes an argument of type Int and returns a String .

What is [+ A in Scala?

It declares the class to be covariant in its generic parameter. For your example, it means that Option[T] is a subtype of Option[S] if T is a subtype of S . So, for example, Option[String] is a subtype of Option[Object] , allowing you to do: val x: Option[String] = Some("a") val y: Option[Object] = x.

What is .type in Scala?

Scala is a statically typed programming language. This means the compiler determines the type of a variable at compile time. Type declaration is a Scala feature that enables us to declare our own types.

What is a function in Scala?

A Scala method is a part of a class which has a name, a signature, optionally some annotations, and some bytecode where as a function in Scala is a complete object which can be assigned to a variable. In other words, a function, which is defined as a member of some object, is called a method.


2 Answers

[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.

like image 57
Don Roby Avatar answered Sep 21 '22 05:09

Don Roby


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).

like image 40
Didier Dupont Avatar answered Sep 19 '22 05:09

Didier Dupont