Part1. Generics and type constraints

We’ve been exposed to the concept of generics since we studied Java. When we are not sure what type a parameter is, we use the generic E instead. Generics are most commonly used in collections to indicate “this is a Collection of what elements to load”.

At the same time, in order to restrict the generic E type, we also formulated the concept of upper bound and lower bound for the generic type, also called type constraint. In Scala, in addition to generic upper and lower bounds, type constraints can be implemented through view demarcation and context demarcation, both of which are related to the implicit conversions and implicit values we learned earlier.

In addition, let T1 and T2 have an inheritance relationship, and P1 and P2 also have an inheritance relationship. Let f1: T2 => P1, and F2: T1 => P2. Can we think that function F2 can replace the function of F1, or can we think that f2 is a subfunction of F1? First of all, in Scala, the answer is yes. The author will introduce the reasons for this understanding from the perspective of the Richter substitution principle in the following article.

Define the first generic class

Java uses Angle brackets <> (also known by some as diamond symbols) to define generics (in Scala it’s often called a “type parameter,” but the meaning is the same), while Scala uses brackets [] to indicate generics. Let’s try the first example: Define a Message class and define the type of information it contains as generic.

class Message[E] (s: E) {
  def get: E = s
}
Copy the code

When you define a generic class or a generic method, you write [] before the argument list.

def getMid[T](list: List[T) :T = {
    list(list.length / 2)}Copy the code

The basic usage of generics is not described here. I’ll focus on Scala’s type constraints section here.

Type constraints

Type constraints are divided into Upper Bounds and Lower Bounds.

In Java’s generics mechanism, if you qualify A generic type E to be A subtype of type A (or if A is an upper bound on the generic type E), the syntax is

. Scala uses [E <: A] (<: is like A logical <=). Also, in Java? Placeholders are represented in Scala with _.

Now, try writing a generic comparison method called Greater in Scala that compares class instances that implement the Comparable interface.

def compareVal[T< :Comparable[T]](t1: T, t2: T) :T = {
  if (t1.compareTo(t2) > 0) t1 else t2
}
Copy the code

Call this function inside the main function, passing two instances of type java.lang.Integer:

println(
	compareVal(java.lang.Integer.valueOf(32), java.lang.Integer.valueOf(12)))Copy the code

Scala’s Int type is not used here because the AnyVal type in Scala does not implement the Comparable interface (which is in the Java category; Scala uses the Ordered attribute). However, Scala provides an implicit conversion function from AnyVal to a Java wrapper class that implements the Comparable interface. Therefore, when we call this function, we need to write that all Scala Int input parameters are implicitly converted to java.lang.Integer before calling this method.

compareVal[java.lang.Integer] (31.21)
Copy the code

Lower bound and upper transition object

In Java’s generics mechanism, if you qualify A generic type E to be the parent of type A (or that A is the lower bound of the generic type E), the syntax is

. Scala uses [E >: A] (>: is similar to the logical >=).

The use of lower bounds can sometimes lead to intuitively unacceptable phenomena. We first declare four classes: Animal, Bird, and Parrot. The three have an inheritance relationship, plus an unrelated Car class.

class Animal {
  def sound() :Unit = println("this is an animal")}class Bird extends Animal {
  override def sound() :Unit = println("this is a bird")
  def fly() :Unit = println("This bird is flying...")}class Parrot extends Bird{
  override def sound() :Unit = println("this is a parrot")
  override def fly() :Unit = println("This parrot is flying...")}class Car

Copy the code

Define a generic function in the main function as follows:

def getBirds[T> :Bird](things : ListBuffer[T) :ListBuffer[T] = things
Copy the code

Obviously, we have a lower bound for this generic T, and intuitively, the element we put inside things should at least be of type Bird or Animal. If the Parrot type is passed in, the compiler should report a compilation error because it breaks the “bottom line” of the lower bound. For example, write it like this:

// As a general rule, if we pass in a Parrot instance that does not conform to the lower bound, this code will report an error.
getBirds(ListBuffer(new Parrot.new Bird)) foreach (_.sound())
Copy the code

In fact, this code is correct. The result of execution is:

this is a parrot
this is a bird
Copy the code

What type of T is in ListBuffer[T] in the above code? Open the interactive terminal REPL, enter all of the above declarations, and execute the following code:

getBirds(ListBuffer(new Parrot.new Bird))
Copy the code

After executing this code, it returns:

scala>  getBirds(ListBuffer(new Parrot.new Bird))
res1: scala.collection.mutable.ListBuffer[Bird] = ListBuffer(Parrot@64aad809, Bird@1f03fba0)
Copy the code

From the results of res1, when we pass in an instance of Parrot, it should be treated as an uptransition object of Bird, so no error is reported.

As mentioned in previous articles, when operationally transforming objects, programs always choose to execute the most “concrete” method possible because of Java’s dynamic binding mechanism. So, the first line of the console prints this is a parrot instead of this is a bird. So let’s do the second experiment, if this list is mixed with a superlative Animal instance. So does the following code work?

getBirds(ListBuffer(new Parrot.new Animal)) foreach (_.fly())
Copy the code

The compiler will simply indicate the error: the fly method cannot be resolved. The REPL shows that the T type is currently considered Animal.

scala> getBirds(ListBuffer(new Parrot,new Animal))
res2: scala.collection.mutable.ListBuffer[Animal] = ListBuffer(Parrot@73afe2b7, Animal@9687f55)
Copy the code

Obviously, in order to keep this list compatible, the generic T must choose the more abstract type between Parrot and Animal, and obviously not all animals have the fly method. If you simply pass in an unrelated class, Car, then T can only be the most abstract Object type.

scala> getBirds(ListBuffer(new Parrot.new Car))
res3: scala.collection.mutable.ListBuffer[Object] = ListBuffer(Parrot@1e495414.Car@3711c71c)
Copy the code

In this case, map(_.sound()) cannot even be called.

Generics chapter summary

Based on the phenomenon in the previous section, the author gives the following derivation:

  1. When inferring type parameters, the interpreter always selects the “superlative” (or “most compatible”) type, up to the Object class.

  2. Since a superclass does not necessarily own a method of a subclass, an inferred instance of type T may not call a method of a subclass.

  3. In contrast to the upper bound, the actual type S of the passed element may be a subclass of the lower bound T, and it will be converted to an upper transition object of type T. Follow Java’s dynamic binding mechanism when calling its methods.

Part2. View definition and context definition

Not only are view and context constraints unique to Scala, but implicit conversions are also heavily used. At the beginning of the example in this chapter, we will replace Java’s Comparator

and Comparable

interfaces with Scala’s Ordered[T] and Ordered[T] attributes.

View definition

The symbol for View Bounds is <%, indicating “View Bounds.” If T <% S exists, then T is not necessarily a strict subclass of S, because we can indirectly provide an implicit conversion function from T to S to establish the relationship “T <: S”. In this case, S can be said to be the horizon of T.

Instead of the previous generic upper and lower bounds, the idea of view delimiting is closer to using implicit conversion functions to convert some mismatched T-types to adaptive types (similar to an adapter pattern).

For example, how do you compare the size between $3 and rmb20? There is no direct comparison between different currencies. But whatever currency they are, their value is always tied to something else: gold. Obviously, the way to compare currencies of different values is to convert them into their gold equivalently, and then to see which one gets more gold.

Here are some definitions of template classes:

abstract class Money

case class RMB(value : Double) extends Money
case class Dollar(value: Double) extends Money

case class Gold(weight: Double) extends Ordered[Gold] {
  override def compare(that: Gold) :Int = {
    val flag: Double = this.weight - that.weight
    if (flag > 0) 1
    else if (flag < 0) - 1
    else 0}}Copy the code

Only Gold implements the Ordered attribute, so only Gold can be compared with each other through the weight attribute. As a bonus, the use of the Ordered attribute is essentially the same as Java’s Comparable

. In addition, any class that implements the Ordered interface can use <, >, >=… And so on.

Let’s define another GT method that determines whether the former currencies are worth more (or equivalent) than the latter by how much they can be converted into gold.

def gt[TThe < %Gold](c1 : T,c2 :T) :Boolean= {// The > method is actually from Ordered[Gold].
  c1 >= c2
}
Copy the code

Where T <% Gold means that other classes T that do not have the ability to compare, such as the Money class in this code block, are allowed to convert to the equivalent Gold recomparison. This requires us to provide the corresponding implicit conversion function:

implicit def Money2Gold(money: Money) : Gold = {
    money match {
        case RMB(value) => Gold(value / 272.00d)
        case Dollar(value) => Gold(value / 38.00d)
    }
}

Copy the code

In this code, we use pattern matching to complete the gold conversion function for different currencies.

println(gt(RMB(20), Dollar(3)))
Copy the code

Then you can compare the different currencies using the GT method. Of course, the T used for comparison can be any other type, and their “value” can be compared as long as the corresponding implicit conversion function is provided to convert them to Gold.

The more recommended way to write it

This code runs without any problems, however the compiler will still pop up a warning. The reason is that it prefers to write the view definition equivalent as an implicit parameter. The GT we just defined is best defined as follows:

def gt[T](c1: T, c2: T) (implicit money2Gold: T= >Gold) :Boolean = c1 >= c2
Copy the code

Writing this way exposes the argument list of the implicit arguments, so that the caller of the code can actively customize the details of the T => Gold function instead of relying solely on the implicit conversion function provided in the domain.

Context demarcation

The context-defined symbol is:, and the form is [T:M]. It is only separated by the < and > sign from the upper <: lower >: symbol, but it represents a very different concept: the context definition [T:M] means that an implicit value (or context) is relied on to give an M[T] and to call some of the functions of M[T].

Let’s start with a concrete example. Declare a function gtOrEq: It compares two Person objects and returns the older one (if both are the same age).

// This method name is used to avoid suspicion... Ordering also has a GTEQ method inside of it.
def gtOrEq[Person](x: Person, y: Person) (implicit ordering: Ordering[Person) :Person = {
    if(ordering.gteq(x, y)) x else y
}
Copy the code

Person is a simple sample class.

case class Person(age: Int)
Copy the code

The gtOrEq method needs to receive a Ordering Person comparator, which we will implement ourselves. Since only one such comparator is needed globally, a singleton is created here using the object keyword.

implicit object PersonComparator extends Ordering[Person] {
    override def compare(x: Person, y: Person) :Int = x.age - y.age
}
Copy the code

Now, the gtOrEq method automatically implements a Person comparison through the PersonComparator.

Obviously, this gtOrEq method is generalisable because it’s not limited to comparing persons. In fact, for any T type, gtOrEq only needs to ensure that it can accept the Ordering[T] provided in the context. And call its GTEQ method to work. This logic can be easily expressed using context definitions and is more concise in writing.

def gtOrEq[T : Ordering](x: T, y: T) :T = {
    if (implicitly[Ordering[T]].gteq(x, y)) x else y
}
Copy the code

No implicit parameter list is defined here. How can gtOrEq call the implicit value inside the function now? All implicit conversions are bound at compile time, and Scala provides Implictly [E] to proactively obtain implicit values in the domain that satisfy type E. The implicitly[Ordering[T]] method is used to obtain the PersonComparator.

Part3

Let A be the superclass of B, and there is another generic class C[T], and give the definitions of invariant, contravariant and covariant.

Invariant *, which means that C[T] : C[A] and C[B] have no affiliation, and that the order of inheritance of C[T] is independent of that of T.

C[+T] : C[A] is the parent class of C[B], and the inheritance order of C[T] is the same as that of T.

C[-t] : C[B] is the parent class of C[A]. The inheritance order of C[T] is the reverse of that of T.

Here, the + and – signs refer to type variable annotations. They can only be used with generic classes, not generic methods, because when we talk about type variants (covariant and contravariant), the invariant concept describes the relationship between a type parameter and the generic class that contains it. In addition, variant annotations can be used with upper <: and lower >: symbols.

Let’s take an example that doesn’t change. In Java, the List class is immutable. This means that List[String] is not a subclass of List[Object], therefore, such an assignment is incorrect:

List<String> list = new LinkedList<Object>();
Copy the code

Now, let’s dive into the Scala code and see what the differences are in these three cases. I start with three very simple class declarations:

class Box[T]
class Bird
class Parrot extends Bird
Copy the code

If this Box is invariant Box[T] :

val box1  : Box[Bird] = new Box[Bird]     // access.
val box2  : Box[Bird] = new Box[Parrot]   // Oops!
val box3  : Box[Parrot] = new Box[Bird]   // Oops!
Copy the code

If this Box is covariant Box[+T] :

val box1  : Box[Bird] = new Box[Bird]     // access.
val box2  : Box[Bird] = new Box[Parrot]   // access.
val box3  : Box[Parrot] = new Box[Bird]   // Oops!
Copy the code

If this Box is contravariant Box[-t] :

val box1  : Box[Bird] = new Box[Bird]     // access.
val box2  : Box[Bird] = new Box[Parrot]   // Oops!
val box3  : Box[Parrot] = new Box[Bird]   // access.
Copy the code

If a class contains more than one generic parameter, there may be both contravariant and covariant relationships, such as:

class Function[-T, +R]
Copy the code

It says that as Function[T,R] continues to inherit, T becomes more abstract (closer to Any) and R becomes more concrete. So, what’s the point of introducing covariant in Scala? In short, Scala wants to provide functions with a feature that can be “inherited and extended” like OOP to achieve “greater FP.”

Go deep into the Richter substitution principle

The Liskov Substitution Principle: It is safe to assume that T is a subtype of type U if a t-type Substitution can be used wherever U is required. In OOP programs, the Richter substitution principle is the most common and relatively easy to understand. Consider the following Java code:

List<String> list = new LinkedList<>();
Copy the code

Obviously, the list is an upper-transition object that uses a more powerful subclass instead of implementing the functionality of its parent class (which is actually an interface). Obviously, LinkedList can do everything that List requires. It satisfies the Richter substitution principle and therefore does not cause any problems in its use.

Conversely, when a LinkedList is needed, giving it a more abstract List is an unsafe piece of code. The reason is that a program may depend on subclasses to provide more sophisticated functionality, but its parent class may not. Unless more detailed code is put on the parent class, such a program itself violates the OCP open closed principle and is not logically recognized.

Describes the “inheritance” relationship between functions

In FP programming, the magnitude of substitution principle of philosophy is that: if the function of T, it can realize U the same function and function, and only need more abstract parameters, will be able to provide more specific values, then we can T think function is the function of U subtypes, or the function T U function more “powerful” than function. Yes, in Scala there are not only up-transition objects, but also “up-transition functions.”

For the time being, the author expresses T as a “subfunction” of U by simple notation: using P1, P2… Represents the types that may appear in the argument list of a function, R1, R2… Represents a possible type in the return value of a function. If U: (P1,P2) => (R1,R2), then T: (↑P1,↑P2) => (↓R1,↓R2). ↑ represents the parent class of the corresponding Pi type, and ↓ represents the child class of the corresponding Ri type.

Obviously, the parameter types here exhibit two phenomena:

  1. The inheritance order of the types in the argument list is reversed from the inheritance of the function itself, because the more powerful the function, the more abstract the arguments.
  2. The inheritance order of the type in the return value is the same as the inheritance order of the function itself, because the more “powerful” the function, the more specific the return value.

To be clear:

  1. The argument list and the function are inverting.
  2. The return value and the function are covariant.

If we wanted to define a functional interface (P1,P2)=>(R1,R2) with the above inheritance relationship, it would look like this:

trait Function2_2[-P1, -P2, +R1, +R2] {
  
  def supply(p1 : P1, p2 : P2) : (R1.R2)}Copy the code

In the description of this class, all PNS appearing in the argument list of the function are contravariant, so this is called the contravariant point. Again, all Rn that appears in the return value is covariant, so the return value here is called the covariant point.

The compiler checks your program: If the arguments that appear in the contravariant point are covariant, or if the arguments that should appear in the contravariant point are contravariant, it will tell you that you have an error like this:

// Invert R1 type appears at the invert point position in... Contravariant type R1 occurs in covariant position in ... // The covariant P1 type appears at the contravariant point position in... Covariant type P1 occurs in contravariant position in ...Copy the code

Why does the compiler intercept this? Because both cases violate the Richter substitution principle between functions:

  1. More “powerful” functions should not consume more specific parameters.
  2. A more “powerful” function should not produce a more abstract return value (less can’t be made).

This principle satisfies the “greedy strategy” of the caller: when he is not sure exactly what arguments a function will take, he will want to pass in only the most generic types to make the function work. In this way, the program caller is not forced to go into the details of the type parameter in order to get the correct call result.

More covetously, the caller wants the return value of the function to be detailed. For example, for a String handling function, programmers would want the function to return a specific String, rather than a crude Any/Object. In this way, the caller does not have to do type checking, or do casting, and many other tedious projects.

In the type definition section below, the position of the type parameter T that does not involve a type variant will become the invariant point, or the position will no longer receive the contravariant or covariant parameter type (described later).

// T is located at a constant point.
class Obj[T]
Copy the code

A type that also has contravariant covariant characteristics

Here, in order to try out possible cases and errors, the design of covariant, inverter is rather “deliberate”. We will not discuss the practical use of such a declaration for the moment, just whether it will cause the compiler to report an error.

In a special case, what if a type parameter K appears in both the parameter list and the return value, and you want to pass in a more abstract type K, so that the output ListBuffer can load a more detailed type K?

// We want K appearing in the argument list to be contravariant, and K appearing in the return value to be covariant.
// There is no ± sign in Scala to indicate that K is both covariant and contravariant.
trait WorkShop[+ / -K]{
	def process(k : K) : ListBuffer[K]}Copy the code

We cannot do this, because it will make K ambiguous, but we can choose the equivalent substitution method, that is, to replace one of the cases with another symbol O, for example, to replace the covariant relationship between WorkShop and K with the symbol O:

trait WorkShop[-K]{
    // Use O<:K equivalent instead of +K.
    / / note!!!!!! O itself is invariant, because it has no type change sign, only an upper bound constraint.
    // Process is modified as a generic function.
	def process[O< :K](k : K) : ListBuffer[O]}Copy the code

Conversely, it seems feasible to replace the covariant relationship between WorkShop and K with the symbol O equivalent:

trait WorkShop[+K]{
    // Use O>:K equivalent instead of -k.
	def process[O> :K](o : O) :ListBuffer[K]}Copy the code

In most cases, equivalent substitution should be the same for both parties, but not in this case. In this transformation, it is reasonable for K to appear at the covariant point of the function. However, K is also the type parameter of ListBuffer, which causes a conflict. There are two reasons:

  1. inWorkShopBy its own definition, it’s the same asKIt’s covariant. Assume thatWorkShop[T]WorkShop[U]Then, according to the Richter substitution principle, call their respectiveprocessmethod-generatedListBuffer[T]Is alsoListBuffer[U]The subclass. To put it more simply, it isWorkShop[+K]thinkListBufferKIt should also be covariant.
  2. However, inListBufferIn its own definition,ListBufferAnd the corresponding location of the type parameter (hereAIn fact, isK) is a constant relationship.
@SerialVersionUID(3419063961353022662L)
final class ListBuffer[A].Copy the code

WorkShop’s “expectation” clearly contradicts the definition of ListBuffer itself, and the compiler reports that ListBuffer does not support loading a covariant K, or that K appears at a constant point.

// The contravariant X appears at the same position at... Contravariant type X occurs in covariant position in ... // The covariant X appears at the constant point at... Covariant type X occurs in invariant position in ...Copy the code

In the earlier example, why did O<:K instead of +K not get an error? Very simple. O is constant, and it appears at any subsequent contravariant point, covariant point, constant point.

Here’s a “positive example” that’s easier to understand: the type parameter K for WorkShop is covariant, and K also appears at the covariant point for Products, so the compiler doesn’t give you an error, but once the relationship between Product and T is contravariant, or invariant, that doesn’t work.

trait WorkShop[+K]{
  // Use O>:K equivalent instead of -k.
  def process[O> :K](o: O) :Products[K]}// The generic parameters for Products are also covariant.
trait Products[+T]
Copy the code

Chapter summary of type variation

If you want to covariant, covariant, to keep a generic class safe from inheritance (or make it conform to the Richter substitution principle), remember five points:

  1. Type variable annotations are used for generic classes; they cannot be used for generic functions.
  2. Any type parameter that appears in the function parameter list should be invert.
  3. Type arguments that appear in the return value of a function should be covariant.
  4. If a certain type parameterTAs a type parameter for other generic classes, you also need to determine whether the type parameter for the corresponding position of the generic class is contravariant or covariant.
  5. Invariant type parameters can be declared at any contravariant or covariant point. Conversely, invariant points cannot appear with any contravariant, covariant type parameter.

Finally, a picture is quoted to vividly describe the relationship of type variation:

A simple actual fight

There are three ingredients (in order of inheritance) : Metal, Iron, Steel, and three products (in order of inheritance) : Product, Vehicle, and Tank.

Package generic. Var1 import Java. Text. SimpleDateFormat import Java. Util. Date / * * * in the broad sense of metal. */ class Metal { def purify() : Unit = println("purifying ..." )} /** * Iron is inherited from metals. */ class Iron extends Metal { def forge() : Unit = println("forging ..." )} /** * Steel is an alloy based on iron, and here the convention is that it inherits from iron. */ class Steel extends Iron { def finishing() : Unit = println("finishing ..." )} /** * products in a broad sense. Have a unique date of manufacture. */ class Product { val dateOfProduction: String = new DateFormat(" YYYy-mm-dd HH: MM :ss").format(new Date)} String = new DateFormat(" YYYy-mm-dd HH: MM :ss").format(new Date)} */ class Vehicle extends Product { def run() : Unit = println("running..." } /** * Tanks inherit from vehicles and have the function of firing guns. */ class Tank extends Vehicle { def fire() : Unit = println("fire") }Copy the code

Declare a Manufacturer factory class with a type parameter, whose raw materials and products are of the six types described above. The plant relies on raw materials and products that are bound by upper bound type parameters M and P. Now use contravariant, covariant, to make this factory conform to the Richter substitution principle and demonstrate how an instance of a factory of type Manufacturer[Iron, Vehicle] can be safely assigned to the Manufacturer[Steel, Product] type.

Here’s the Manufacturer statement:

abstract class Manufacturer[-M <: Metal, +P <: // Simple Abstract Method = Simple Abstract Method = Simple Abstract Method = Simple Abstract Method = Simple Abstract Method = Simple Abstract Method = Simple Abstract Method = Simple Abstract Method // For SAM, we can write a short version of the anonymous implementation class in Scala, similar to the way Java 8 Lambda is written. // But make sure your Scala version is 2.12+. def convert(m: M): P }Copy the code

The main function is as follows.

/* Note: In Scala 2.12, SAM can have the following shorthand. var manufacturer: Manufacturer[Steel, Product] = (m: Steel) => { m.purify() m.forge() m.finishing() new Product } var manufacturer2: Manufacturer[Iron, Vehicle] = (i: Iron) => { i.purify() i.forge() new Vehicle } */ var manufacturer: Manufacturer[Steel, Product] = new Manufacturer[Steel, Product] { override def convert(m: Steel): Product = { m.purify() m.forge() m.finishing() new Product } } val manufacturer2: Manufacturer[Iron, Vehicle] = new Manufacturer[Iron, Vehicle] { override def convert(m: Iron): Vehicle = {m.purify() m.battery () new Vehicle}} manufacturer.convert(new Steel) manufacturer2 can use the manufacturer2 system. Make more refined products. Manufacturer = Manufacturer2 // Therefore, manufacture work is not affected and can be performed normally. manufacturer.convert(new Steel)Copy the code