© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
D. Pollak et al.Beginning Scala 3https://doi.org/10.1007/978-1-4842-7422-4_8

8. Scala Type System

David Pollak1  , Vishal Layka2 and Andres Sacco3
(1)
San Francisco, CA, USA
(2)
Bruxelles, Belgium
(3)
Ciudad Autonoma de Buenos Aires, Argentina
 

The two fundamental design considerations of a programming language are static versus dynamic typing and strong versus weak typing. Types in a programming language are checked at compile time and can be inferred by a compiler. Scala is a strong and statically typed language with a unified type system.

In static typing, a variable is bound to a particular type. In dynamic typing, the type is bound to the value instead of the variable. Scala and Java are statically typed languages, whereas JavaScript, Python, Groovy, and Ruby are dynamically typed languages.

If a type is static and strongly typed, every variable must have a definite type. If a type is dynamic and strongly typed, every value must have a definite type. However, in the case of weak typing, a definite type is not defined. Scala, Java, and Ruby are principally strongly typed languages. Some languages, such as C and Perl, are weakly typed.

Scala is the best of both worlds, in that it feels like a dynamically typed language, because of type inference, and at the same time, it gives you all the benefits of static typing in terms of an advanced object model and an advanced type system.

This chapter explores ideas such as which type parameters should be covariant, contravariant, or invariant under subtyping, using implicit judiciously, and so forth.

Unified Type System

Scala has a unified type system , enclosed by the type Any at the top of the hierarchy and the type Nothing at the bottom of the hierarchy, as illustrated in Figure 8-1. All Scala types inherit from Any. The subtypes of Any are AnyVal (value types, such as Int and Boolean) and AnyRef (reference types, as in Java). As you can see in Figure 8-1, the primitive types of Java are enclosed under AnyVal and, unlike Java, you can define your own AnyVal. And also unlike Java, Scala does not have wrapper types, such as Integer, to be distinguished from primitive types, such as int.
Figure 8-1

Unified object model

As you can see in Figure 8-1, Any is a supertype of both AnyRef and AnyVal. AnyRef corresponds to java.lang.Object and is the supertype of all objects. AnyVal, on the other hand, represents the value, such as int and other JVM primitives. Because of this hierarchy, it becomes possible to define methods that take Any, thus being compatible with both scala.Int instances as well as java.lang.String , as shown here:
scala> import scala.collection.mutable.ListBuffer
import scala.collection.mutable.ListBuffer
scala> val list = ListBuffer[Any]()
list: scala.collection.mutable.ListBuffer[Any] = ListBuffer()
scala> val x= 2
x: Int = 2
scala> list += x
res12: list.type = ListBuffer(2)
scala> class Book
defined class Book
scala> list += new Book()
res13: list.type = ListBuffer(2, Book@15e8485)
You can limit a method to only be able to work on Value types, as seen here:
def test(int: AnyVal) = ()
test(5)
test(5.12)
test(new Object)
In this code, test(5) takes an Int that extends AnyVal, and test(5.12) takes a Double that also extends AnyVal. Test(new Object) takes an Object that extends AnyRef. Refer to Figure 8-1. Test(new Object) fails to compile.
scala> def test(int: AnyVal) = ()
test: (int: AnyVal)Unit
scala> test(5)
scala> test(5.12)
scala> test(new Object)
<console>:9: error: type mismatch;
-- Error:
1 |test(new Object)
  |     ^^^^^^^^^^
  |the result of an implicit conversion must be more specific than AnyVal
The idea is that this method will only take Value classes, be it Int or your own Value type. This implies that Java code is not as type-safe as Scala code. You’re probably thinking, “But Java is a statically typed language. Doesn’t it give me all the safety that Scala does?” The answer is no. Take a look at the following code and spot the problem:
public class Bad {
    public static void main(String[] argv) {
        Object[] a = argv;
        a[0] = new Object();
    }
}
This is legal Java code, and here’s what happens when you run the code:
> java Bad Hello
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Object
at Bad.main(Bad.java:4)

Java allows you to assign a String[] to Object[] because a String is a subclass of Object, so if the array was read-only, the assignment would make sense. However, the array can be modified. The modification demonstrated shows one of Java’s “type-unsafety” features. We’ll discuss why this happened and the complex topic of invariant, covariant, and contravariant types later in this chapter. Let’s start looking at how Scala makes the architect’s job easier and also makes the coder’s job easier.

Type Parameterization

Scala’s parameterized types are similar to generics in Java. If you are familiar with Java or C#, you might already have some understanding of parameterized types. Scala’s parametrized types provide the same features as Java generics, but with extended functionalities.

Note

Classes and traits that take type parameters are called generic; the types they generate are called parameterized types.

One straightforward syntactical difference is that Scala uses square brackets ([...]), while Java uses angle brackets (<...>). For example, a list of strings would be declared as shown here:
val list : List[String] = List("A", "B", "C")

Scala allows angle brackets to be used in the method name. So, to avoid ambiguities, Scala uses square brackets for parameterized types.

Types in Scala are used to define classes, abstract classes, traits, objects, and functions. Type parameterization lets you make them generic. As an example, Sets can be defined as generic in the following manner: Set[T]. However, unlike Java which allows raw types, in Scala you are required to specify type parameters. That is to say, Set[T] is a trait but not a type because it takes a type parameter.

As a result, you cannot create variables of type Set.
scala> def test(s: Set) ={}
-- Error:
1 |def test(s: Set) ={}
  |            ^^^
  |            Missing type parameter for Set
Instead, trait Set enables you to specify parameterized types, such as Set[String], Set[Int], or Set[AnyRef].
scala> def test(s: Set[AnyRef]) = {}
def test(s: Set[AnyRef]): Unit

For example, trait Set defines a generic set where the specific sets are Set[Int] and Set[String] and so forth. Thus, Set is a trait and Set[String] is a type. The Set is a generic trait.

Note

In Scala, List, Set, and so on can also be referred to as type constructors because they are used to create specific types. You can construct a type by specifying a type parameter. For example, List is the type constructor for List[String] and List[String] is a type. While Java allows raw types, Scala requires that you specify type parameters and does not allow you to use just a List in the place of a type, as it’s expecting a real typenot a type constructor.

In light of inheritance, type parameters raise an important question regarding whether Set[String] should be considered a subtype of Set[AnyRef]. That is, if S is a subtype of type T, then should Set[S] be considered a subtype of Set[T]? Next, you will learn a generic type concept that defines the inheritance relation and answers the aforementioned question.

Variance

Variance defines inheritance relationships of parameterized types, which brings to light whether a Set[String], for example, is a subtype of Set[AnyRef]. A declaration like class Set[+A] means that Set is parameterized by a type A. The + is called a variance annotation.

Variance is an important and challenging concept. It defines the rules by which parameterized types can be passed as parameters. In the beginning of the chapter, we showed how passing a String[] (Java notation) to a method expecting an Object[] can cause problems. Java allows you to pass an array of something to a method expecting an array of something’s superclass. This is called covariance. On the surface, this makes a lot of sense. If you can pass a String to a method expecting an Object, why can’t you pass an Array[String] (Scala notation) to a method expecting an Array[Object]? Because Array is mutable; it can be written to in addition to being read from, so a method that takes an Array[Object] may modify the Array by inserting something that cannot be inserted into an Array[String].

Defining the type variance for type parameters allows you to control how parameterized types can be passed to methods. The variance comes in three flavors: invariant, covariant, and contravariant. Type parameters can be individually marked as covariant or contravariant and are by default invariant. Variance in Scala is defined by using + and - signs in front of type parameters.

Covariant Parameter Types

Covariant parameter types are designated with a + before the type parameter. A covariant type is useful for read-only containers. Scala’s List is defined as List[+T], which means that it’s covariant on type T. List is covariant because if you pass a List[String] to a method that expects a List[Any], then every element of the List satisfies the requirement that is an Any and you cannot change the contents of the List. Figure 8-2 gives a very clear picture of covariance, such as if S extends T, then Class[S] extends Class[T].
Figure 8-2

Covariance in Scala

Tip

Covariance: If S extends T then Class[S] extends Class[T].

Let’s define an immutable class, Getable. Once an instance of Getable is created, it cannot change, so you can mark its type, T, as covariant.
scala> class Getable[+T](val data: T)
defined class Getable
Let’s define a method that takes a Getable[Any].
scala> def get(in: Getable[Any]) =
     |      println("It's "+in.data)
def get(in: Getable[Any]): Unit
You define an instance of Getable[String] here:
scala> val gs = Getable("String")
gs: Getable[java.lang.String] = Getable@10a69f0
You can call get with gs.
scala> get(gs)
It's String

Let’s try the same example but pass a Getable[java.lang.Double] into something that expects a Getable[Number].

scala> def getNum(in: Getable[Number]) = in.data.intValue
getNum: (Getable[java.lang.Number])Int
scala> def gd = Getable(java.lang.Double.valueOf(33.3))
gd: Getable[java.lang.Double]
scala> getNum(gd)
res7: Int = 33

Yes, the covariance works the way you expect it to. You can make read-only classes covariant. This means that contravariance is good for write-only classes.

Contravariant Parameter Types

So, if covariance allows you to pass List[String] to a method that expects List[Any], what good is contravariance? Contravariance indicates if S extends T, then Class[T] extends Class[S], as illustrated in Figure 8-3.
Figure 8-3

 Contravariance in Scala

Tip

Contravariance: If S extends T, then Class[T] extends Class[S].

Let’s first look at a write-only class, Putable.
scala> class Putable[-T]:
     |     def put(in: T) =
     |         println("Putting "+in)
     |
// defined class Putable
Next, let’s define a method that takes a Putable[String].
scala> def writeOnly(in: Putable[String])=
     |     in.put("Hello")
def writeOnly(in: Putable[String]): Unit
And let’s declare an instance of Putable[AnyRef].
scala> val p = Putable[AnyRef]
val p: Putable[AnyRef] = Putable@fb79241
And what happens if you try to call writeOnly?
scala> writeOnly(p)
Putting Hello

Okay, so you can call a method that expects a Putable[String] with a Putable[AnyRef] because you are guaranteed to call the put method with a String, which is a subclass of AnyRef. Standing alone, this is not particularly valuable, but if you have a class that does something with input that results in output, the value of contravariance becomes obvious.

The inputs to a transformation are contravariant. Calling something that expects at least any AnyRef with a String is legal and valid. But the return value can be covariant because you expect to get back a number, so if you get an Integer, a subclass of Numbers, you’re okay. Let’s see how it works. You define DS with a contravariant In type and a covariant Out type.
scala> trait DS[-In, +Out]:
     |     def apply(i: In): Out
     |
// defined trait DS
Let’s create an instance that will convert Any into an Int.
scala>  val t1 = new DS[Any, Int]{def apply(i: Any) = i.toString.toInt}
val t1: DS[Any, Int] = anon$1@430aae8e
After that, create a method that invoke apply method on the trait:
scala> def check(in: DS[String, Any]) = in("333")
def check(in: DS[String, Any]): Any
And you call to check with t1.
scala> check(t1)
res14: Any = 333

Invariant Parameter Types

In Scala, Array[T] is invariant. This means that you can only pass an Array[String] to foo(a: Array[String]) and that you can only pass an Array[Object] to bar(a: Array[Object]). Figure 8-4 gives a clear picture of invariant parameter types.
Figure 8-4

 Invariance in Scala

This ensures that what is read from or written to the array is something of the correct type. So, for anything that’s mutable, the type parameter should be invariant. You do this by doing nothing with the type parameter. So, let’s define an invariant class.
class Holder[T](var data: T)

The class holds data of type T. Let’s write a method.

scala> def add(in: Holder[Int])= in.data = in.data + 1
def add(in: Holder[Int]): Unit
scala> val h = Holder(0)
val h: Holder[Int] = Holder(0)
scala> add(h)
scala> h.data
val res1: Int = 1
Because the add method expects an Int to come out of Holder and puts an Int back into Holder, the type of Holder must be invariant. This does not mean that invariant containers lose their ability to hold subclasses of their declared type. Holder[Number] can contain a Double, and an Array[Object] can contain String, Integer, and so on. Let’s put a Double into Holder[Number]:
scala> val nh = Holder[Number](33.3d)
val nh: Holder[Number] = Holder(33.3)
Now define a method that rounds the number.
scala> def round(in: Holder[Number]) =
     |     in.data = in.data.intValue
def round(in: Holder[Number]): Unit
Call the round method and let’s see what you get out the other side.
scala> round(nh)
scala> nh.data
res16: java.lang.Number = 33
You put in a Number and got back a Number. What’s the underlying class for Number?
scala> nh.data.getClass
res17: java.lang.Class[_] = class java.lang.Integer
An Integer is a subclass of Number, so you can put an Integer or a Double into Holder[Number]. You reserve the ability to use class hierarchies with invariant type parameters. Let’s finally see what happens when you try to pass a Holder[Double] into round.
scala>  val dh = Holder(33.3d)
val dh: Holder[Double] = Holder(33.3)
scala> round(dh)
-- Error:
1 |round(dh)
  |      ^^
  |      Found:    (dh : Holder[Double])
  |      Required: Holder[Number]

So, invariant type parameters protect you when you have mutable data structures such as arrays.

Rules of Variance

So, you’ve successfully defined and used an invariant type. The invariant type was mutable, so it both returned and was called with a particular type. You created a covariant type that was an immutable holder of a value. Finally, you created a transformer that had contravariant input and covariant output. Wait, that sounds like a function. That’s right, Scala’s FunctionN traits have contravariant parameters and covariant results. This leads us to the simple rules of variance:
  • Mutable containers should be invariant.

  • Immutable containers should be covariant.

  • Inputs to transformations should be contravariant, and outputs from transformations should be covariant.

Type Bounds

When defining a parameterized type, bounds allow you to place restrictions on type parameters. Thus, a bounded type is restricted to a specific type or its derived type.

Upper Type Bounds

An upper bound type is restricted to a specific type or one of its derived types. Scala provides the upper bound relation operator (<:), which you can use to specify an upper bound for a type.

The type parameter A <: AnyRef means any type A that is a subtype of AnyRef. The <: operator signifies that the type to the left of the <: operator must be a subtype of the type to the right of the <: operator. Moreover, the type on the left of the <: operator could be the same type as the right of the <: operator.

The type parameter A <: AnyRef means that the type to the left of the <: operator must be derived from the type to the right of the <: operator or the type to the left of the <: operator could be the same type as the right of the <: operator. In other words, the upper type bounds (and, as we will explain in the next section, lower type bounds) restrict the allowed types that can be used for a type parameter when instantiating a type from a parameterized type, as illustrated in the following code.
Trait Test:
    def test[A <: AnyRef]: Unit

In the code above, the upper type bound says that any type used for parameter A must be a subtype of AnyRef.

The upper type bound is different from type variance in that type variance determines how actual types of the type are related, for example how the actual types List[AnyRef] and List[String] of the type List are related. Let’s explore this with examples.

Define an Employee class hierarchy.
scala> class Employee (val name: String)
defined class Employee
scala> class Internal (name: String) extends Employee(name)
defined class Internal
scala> class FreeLancer(name: String) extends Employee(name)
defined class FreeLancer
scala> class Customer (name: String)
defined class Customer
Now define a function that takes a parameter with an upper bound.
scala> def employeeName [A <: Employee](emp: A) = println(emp.name)
def employeeName[A <: Employee](emp: A): Unit
Now test the employeeName.
scala> employeeName (Internal("Paul"))
Paul
Now test with FreeLancer.
scala> employeeName (FreeLancer("John"))
John
Now test with the Customer class.
scala> employeeName(Customer("Peter"))
-- Error:
1 |employeeName(Customer("Peter"))
  |             ^^^^^^^^^^^^^^^^^
  |             Found:    Customer
  |             Required: Employee
  |
  |             The following import might make progress towards fixing the problem:
  |
  |               import collection.Searching.search

As you can see, because of the upper bound restriction, this code does not compile, as the Customer class is not a subtype of Employee.

Lower Type Bounds

A lower bound type is restricted to a specific type of its supertype. The type selected must be equal to or a supertype of the lower bound restriction. The following defines a lower type bound:
class A{
    type B >: List[Int]
    def someMethod(a : B) = a
}
You define type B inside class A to have a lower bound of List[Int]. You instantiate a variable st as a subtype of A as shown here:
scala> val st = A { type B = Traversable[Int] }
val st: A{B = Iterable[Int]} = anon$1@3ef3f661

You can call some method with a Set class. This is because Set, even if not a supertype of the List class, is a subtype of Traversable.

Extension Methods

Scala 2 lets you add new methods to some types using the keyword implicit, but in Scala 3, this feature has had modifications and now requires using two different keywords like using/given.

The following sections explain both approaches of the same feature. This is important because you may see Scala 2 code in some projects.

Scala 2 Implicit Class

Using types, especially when type inferencing makes them invisible, is simple and doesn’t take a lot of thought away from the task at hand. Well-defined types and type interactions will stay out of the library consumer’s way but will guard against program errors.

You’ve seen a little bit of stuff so far that looks like magic. The String class seems to have grown methods:
scala> "Hello".toList
res0: List[Char] = List(H, e, l, l, o)
You may be wondering how a Java class that is final could have additional methods on it. Well, Scala has a feature called an implicit conversion. If you have an instance of a particular type, and you need another type, and there’s an implicit conversion in scope, Scala will call the implicit method to perform the conversion. For example, some date-related methods take Long and some take java.util.Date. It’s useful to have conversions between the two. Let’s create a method that calculates the number of days based on a Long containing a millisecond count.
scala> def millisToDays(in: Long): Int = (in / (1000L * 3600L * 24L)).toInt
You can calculate the number of days by passing a Long to the method.
scala> millisToDays(5949440999L)
res3: Int = 68
Let’s try to pass a Date to the method.
scala> import java.util.Date
import java.util.Date
scala> millisToDays(Date().getTime())
val res2: Int = 18931
But sometimes it’s valuable to convert between one type and another. We are used to the conversion in some contexts: Int ➤ Long , Int ➤ Double, and so on. Let’s define a method that will automatically be called when you need the conversion.
scala> import scala.language.implicitConversions
scala> implicit def dateToLong(d: Date):Long = d.getTime
def dateToLong(d: java.util.Date): Long
And this allows you to call millisToDays with a Date instance.
scala> millisToDays(Date())
val res3: Int = 18931
You may think that implicit conversions are dangerous and reduce type safety. In some cases, this is true. You should be very careful with them, and their use should be an explicit design choice. However, sometimes implicit conversions (e.g., Int ➤ Long) are very valuable, such as when you have a method that takes a parameter that must be a Long.
scala> def m2[T <: Long](in: T): Int = (in / (1000L * 3600L * 24L)).toInt
def m2[T <: Long](in: T): Int
scala> m2(33.toLong)
res8: Int = 0
What is the scope of implicit? The Scala compiler considers an implicit in the current scope if
  • The implicit is defined in the current class or in a superclass.

  • The implicit is defined in a traitor super trait or is mixed into the current class or a superclass.

  • The implicit is defined on the companion object of the current target class.

  • The implicit is available on an object that has been imported into the current scope.

When designing libraries, be careful about defining implicits and make sure they are in as narrow a scope as is reasonable. When consuming libraries, make sure the implicits defined in the objects are narrow enough and are not going to cause problems such as getting stuff from every Option.

Implicit conversions are powerful tools and potentially very dangerous. We mean wicked dangerous. Back in the day, we put the following implicit into a library:
implicit def oToT[T](in: Option[T]): T = in.get
This was convenient, very convenient. We no longer had to test Options. We just passed them around, and they were converted from an Option to their underlying type. And when we removed the implicit, we had 150 code changes to make. That was 150 latent defects. Using implicits to convert to a class that has a particular method is a good reason. There’s very little likelihood of damage.
scala> implicit def oToT[T](in: Option[T]): T = in.get
def oToT[T](in: Option[T]): T

Until Scala 2.10, implicit conversion was handled by implicit def methods that took the original instance and returned a new instance of the desired type. Implicit methods have been supplanted by implicit classes, which provide a safer and more limited scope for converting existing instances.

Scala 2.10 introduced a new feature called implicit classes. An implicit class is a class marked with the implicit keyword . This keyword makes the class’s primary constructor available for implicit conversions when the class is in scope.

To create an implicit class, simply place the implicit keyword in front of an appropriate class. Here’s an example:
scala> object Helper:
     |    implicit class Greeting(val x: Int):
     |        def greet = "Hello " + x
     |
// defined object Helper
To use this class, just import it into the scope and call the greet method .
scala> import Helper._
scala> println(3.greet)
Hello 3

For an implicit class to work, its name must be in scope and unambiguous, like any other implicit value or conversion.

Implicit classes have the following restrictions:
  • They must be defined inside another trait/class/object.

object Helpers:
    implicit class RichInt(x: Int) // OK!
implicit class RichDouble(x: Double) // WRONG!
  • They may only take one non-implicit argument in their constructor.

implicit class RichDate(date: java.util.Date) // OK!
implicit class Indexer[T](collecton: Seq[T], index: Int) // WRONG!
implicit class Indexer[T](collection: Seq[T])(implicit index: Index) // OK!
While it’s possible to create an implicit class with more than one non-implicit argument, such classes aren’t used during implicit lookup.
  • There may not be any method, member, or object in scope with the same name as the implicit class. This means an implicit class cannot be a case class.

object Bar
implicit class Bar(x: Int) // WRONG!
val x = 5
implicit class x(y: Int) // WRONG!
implicit case class Baz(x: Int) // WRONG!

Scala 3 Given/Using Clauses

As you read at the beginning of this section, the feature of implicit introduces some changes in Scala 3 which differ from the previous version. In the following sections, you will see some of them.

Implicit Conversions

This feature reduces the complexity, but it does not disappear completely. Let’s see an example that takes a Double and converts it into a new type.

scala> import scala.language.implicitConversions
     |
     | case class Euros(amount: Double):
     |     override def toString = f"$$$amount%.2f"
     |
     | given Conversion[Double,Euros] = d => Euros(d)
// defined case class Euros
lazy val given_Conversion_Double_Euros: Conversion[Double, Euros]
scala> given_Conversion_Double_Euros(3)
val res7: Euros = $3.00

Take into consideration that you can create more than one implicit conversion using given. One last thing: You can continue using the mechanism of implicit at least in version 3.0.1 but consider migrating these functionalities to the new version of the feature because Scala will deprecate some functionalities that are not supported in the latest version.

There are some considerations when using this feature:
  • You can’t chain the use of given to transform into something intermediate. You can only convert from one type to another one.

  • You can only use the conversion declared in the same scope.

Givens and Imports

As you read in the previous section, one of the restrictions of the use of given sentences is the need to be in the same scope. Scala 3 offers a way to import and include in the same scope.
object A:
    val name = "0"
    def hello(s: String) = s"$s, hello from $name"
    class A1
    class A2
    class A3
    given a1: A1 = A1()
    given a2: A2 = A2()
    given a3: A3 = A3()
Let’s see some possible ways to import the given sentences:
import A.{given _} // Imports ONLY the givens
import A.{given a1} // Imports explicitly a1
import A.{given _, _} // Imports everything in A
import A._ // Imports everything EXCEPT the givens
import A.*       // Imports everything EXCEPT the givens
import A.given   // Imports ONLY the givens

Using this type of import, you can declare in an explicit way which things you need to use in your classes or objects.

Using Clauses

In Scala, it is not necessary to explicitly pass parameters to a method. In previous version of Scala, this functionality is connected directly with implicit , but in Scala 3.x.x, the same functionality has a new keyword named using.

To explain this concept well, let’s create some types of order.
trait Ord[T]:
  def compare(x: T, y: T): Int
  extension (x: T) def < (y: T) = compare(x, y) < 0
  extension (x: T) def > (y: T) = compare(x, y) > 0
given intOrd: Ord[Int] with
  def compare(x: Int, y: Int) =
    if x < y then -1 else if x > y then +1 else 0
given listOrd[T](using ord: Ord[T]): Ord[List[T]] with
  def compare(xs: List[T], ys: List[T]): Int = (xs, ys) match
    case (Nil, Nil) => 0
    case (Nil, _) => -1
    case (_, Nil) => +1
    case (x :: xs1, y :: ys1) =>
      val fst = ord.compare(x, y)
      if fst != 0 then fst else compare(xs1, ys1)
Now create a method that receives numbers and, depending on the type of order, obtains the maximum value.
def max[T](x: T, y: T)(using ord: Ord[T]): T =
  if ord.compare(x, y) < 0 then y else x
As you can see, your method receives two parameters of the same type and does not use a specific type of Ord, which you can define when you invoke another part of the code.
scala> max(1, 9)(using intOrd)
val res8: Int = 9
scala> max(List(2,5, 1), List(1,2,3))(using listOrd)
val res11: List[Int] = List(2, 5, 1)

Summary

In this chapter, you learned Scala’s rules about variance, type bounds, and given/using classes.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.0.37