scala learning notes - type parameters

Posted by mbhcool on Tue, 08 Feb 2022 06:46:44 +0100

Multiple definition

Type variables can have both upper and lower bounds. Written as:

T >: Lower <: Upper

There cannot be multiple upper bounds or multiple lower bounds at the same time; However, you can still require a type to implement multiple characteristics, like this:

T <: Comparable[T] with Serializable with Cloneable

There can be multiple context definitions:

T : Ordering : ClassTag

Type constraints

Type constraints provide you with another way to qualify types. There are three relationships available:

T =:= U // Is T equal to U
T <:< U // Is it a subtype of U
T => U // Can it be converted to U

These constraints will test whether T is equal to U, whether it is a subtype of U, or whether it can be converted to U. to use such a constraint, you need to add "implicit evidence parameter", like this:

class Pair[T](val first: T, val second: T)(implicit ev: <:< Comparable[T])

In the previous example, using type constraints does not bring more advantages than type defining class pair [T <: comparable [t], but type constraints can be useful in some specific scenarios.
Type constraints allow you to define methods in generic classes that can only be used under specific conditions. Here is an example:

class Pair[T](val first: T, val second: T){
  def smaller(implicit ev: T <:< Ordered[T]) = if(first < second) first else second
}

You can construct Pair[URL], although the URL is not in order. An error will be reported only when you call the smaller method.
Another example is the orNull method of the Option class:

val friends = Map("Fred" -> "Barney", ...)
val friendOpt = friends.get("Wilma") //This is an Option[String]
val friendOrNull = friendOpt.orNull // Either String or null

When calling each other with java code, the orNull method is very useful, because it is usually used to use null in Java to indicate the lack of a value, but this method does not apply to value types, such as Int. they do not regard null as a legal value, because the implementation of orNull has the constraint null <: < A, you can still instantiate Option[Int], as long as you don't use orNull for these instances.
Another use of type constraints is to improve type inference. For example:

def firstLast[A, C <: Iterable[A]](it: C) = (it.head, it.last)
// When executing the following code:
firstLast(List(1, 2, 3))

You will get A message that the inferred type parameter [Nothing, List[Int]] does not conform to [C <: iteratable [A]]. Why Nothing? The type inference device cannot judge what A is by List (1, 2, 3) alone, because it matches A and C in the same step. To help it solve this problem, first match C and then match A:

def firstLast[A, C](it: C)(implicit ev: C <:< Iterable[A]) = (it.head, it.last)

There are two methods to check whether the sequences of cords correspond to each other:

def corresponds[B](that: Seq[B])(match: (A, B) => Boolean): Boolean

The premise of match is a coriolised parameter, so the type inference device can first determine type B, and then use this information to analyze match. In the following call:

Array("Hello", "Fred").corresponds(Array(5, 4))(_.length == _)

The compiler can infer Int to understand length == _ What's going on.

Type change

Suppose we have a function that handles Pair[Person]:

def makeFriends(p: Pair[Person])

If Student is a subclass of Person, can I call makeFriends with Pair[Student] as a parameter? By default, this is an error. Although Student is a subtype of Person, there is no relationship between Pair[Student] and Pair[Person]. If you want such a relationship, you must indicate this in defining Pair:

class Pair[+T](val first: T, val second: T)

+Sign means that the type is covariant with T, that is, it changes in the same direction as T. since Student is a subtype of Person, Pair[Student] is also a subtype of Pair[Person].
You can also have a variant in another direction. Consider the generic type Friend[T], which means people who want to become friends with people of type T.

trait Friend[-T]{
  def befriend(someone: T)
}

Now suppose you have a function:

def makeFriendWith(s: Student, f: Friend[Student]){ f.befriend(s) }

Can you call it with Friend[Person] as a parameter? That is, if you have:

class Person extends Friend[Person]{ ... } 
class Student extends Person 
val susan = new Student 
val fred = new Person

Can the function call makeFriendWith(susan, fred) succeed? It seems that it should be successful. If Fred is willing to make friends with anyone, he will also want to be susan's friend.
Note that the direction of type change is opposite to that of subtype. Student is a subtype of Person, but Friend[Student] is a supertype of Friend[Person]. In this case, you need to declare the type parameter as contravariant:

trait Friend[-T]{
  def befriend(someone: T)
}

You can use both variants in a generic type declaration. For example, the type of single parameter function is Function1[-A, +R]. To understand why such a declaration is correct, consider the following functions:

def friends(students: Array[Student], find: Function1[Student, Person]) = 
    // You can write the second parameter as find: student = > person 
    for (s <- students) yield find(s)

Suppose you have a function:

def findStudent(p: Person): Student

Can you call friends with this function? Certainly. It is willing to accept any Person, so of course it is also willing to accept Student. It will produce Student results, which can be put into Array[Person].

Covariant and inverse points

Functions are inverse in parameters and covariant in return values. Generally speaking, for the value consumed by an object, inversion is applicable, while for the value produced by it, covariance is applicable.
If an object consumes and produces a value at the same time, the type should remain unchanged. This is usually applicable to variable data structures. For example, arrays in Scala do not support type variation. You cannot convert an Array[Student] into Array[Person], and vice versa. This will be unsafe. Consider the following situations:

val students = new Array[Student](length) 
val people: Array[Person] = students // We can assume that it is illegal, but we can
people(O) = new Person("Fred") // Now students(O) is no longer a Student
// Conversely,
val people = new Array[Person](length) 
val students: Array[Student] = people // Illegal, but assuming we can
people(O) = new Person("Fred") // Now students(0) is no longer a Student

Note: in Java, we can convert Student [] array into Person [] array, but if you try to add objects of non Student class to the array, you will throw ArrayStoreException. In Scala, the compiler rejects programs that may cause type errors.
If we try to declare a covariant variable dual, we will find that this is not feasible. It will be an array with two elements, but it will report the error you saw just now. Indeed, if you use:

class Pair[+T](var first: T, var second: T) //error

You will get an error, saying that the covariant type T appears at the inverse point in the following setter method:

first_=(value: T)

The position of the parameter is the inverse point, and the position of the return type is the covariant point.
However, in the function parameters, the type change is reversed, and its parameters are covariant. For example, the foldLeft method of Iterable[+A] below:

foldLeft[B](z: B)(op: (B, A) => B): B 
                       -  +     + - +

Notice that A is now at the covariant point.
These rules are simple and safe, but sometimes they prevent us from doing things that are not at risk. replaceFirst method for immutable duality:

class Pair[+T](val first: T, val second: T) { 
  def replaceFirst (newFirst: T) = new Pair[T](newFirst, second) // error
}

The compiler rejects the above code because type T appears at the inverse point. However, this method cannot destroy the original duality, and it returns a new duality. The solution is to add another type parameter to the method, like this:

def replaceFirst[R >: T](newFirst: R) = new Pair[R](newFirst, second)

In this way, the method becomes a generic method with another type parameter R, but R is constant, so there will be no problem at the inverse point.

Object cannot be generic

We can't add type parameters to objects; For example, variable list. The list of element type T is either empty or a node with header type T and tail type List[T]:

abstract class List[+T] { 
	def isEmpty: Boolean 
	def head: T 
	def tail: List[T]
}
class Node[T](val head: T, val tail: List[T]) extends List[T] { 
	def isEmpty = false
}
class Empty[T] extends List[T] { 
	def isEmpty = true 
	def head = throw new UnsupportedOperationException 
	def tail = throw new UnsupportedOperationException
}

Note: Node and Empty are used here to make the discussion easier for Java programmers. If you're familiar with Scala lists, just replace them with:: and Nil in your mind.
Defining Empty as a class looks silly because it has no state, but you can't simply turn it into an object:

object Empty[T] extends List[T] // error

You cannot add a parameterized type to an object. In this case, the solution is to inherit List[Nothing]:

object Empty extends List[Nothing]

Nothing type is a subtype of all types. Therefore, when we construct the following single element list,
val lst= new Node(42, Empty )
The type check was successful. According to the covariant rule, List[Nothing] can be converted into List[Int], so the constructor of Node[Int] can be called.

Type wildcard

In Java, all generic classes are immutable, but you can use wildcards to change their types, for example, methods

void makeFriends (List<? extends Person> people)

You can call it with list < student > as a parameter.
You can also use wildcards in Scala. They look like this:

def process(people: java.util.List[ _ <: Person ])

In Scala, you don't need to use wildcards for covariant Pair classes. But suppose Pair is constant:

class Pair[T](var first: T, var second: T)

Then you can define:

def makeFriends(p: Pair[ _ <: Person]) // It can be called with Pair[Student]

You can also use wildcards for the inverse:

import java.util.Comparator
def min[T](p: Pair[T])(comp: Comparator[ _ >: T])

The type wildcard is used to refer to the "syntax sugar" of the existing type.
Note: in some specific complex situations, Scala's type wildcards are not perfect. For example, the following statement is in scala2 12 is not feasible:

def min[T <: Comparable[ _ >: T]](p: Pair[T]) = ...

The solution is as follows:

type SuperComparable[T] = Comparable[ _ >: T] 
def min[T <: SuperComparable[T]] (p: Pair[T]) = ...

Topics: Scala Big Data Spark Data Analysis