I touched Scala ~ [Type parameters and displacement specification] ~

Introduction

image.png

It's a direct migration of the dwango tutorial, where you'll study, edit it, and replace it with your own terms.

Type parameter

Although not mentioned in the class section, a class can take zero or more types as parameters. This is useful when you want to represent a type that you cannot identify at the time you create a class (for example, the type of an element in a collection class). The grammar of the class definition including the type parameters is as follows.


class <name of the class>[<Type parameter 1>, <Type parameter 2>, ...](<Classarguments>) {
  (<Field definition>|<Method definition>)*
}

You can give each type parameter sequence any name you like and use it in the class definition. In the Scala language, it is customary to name A, B, ... from the beginning, so it is safe to match it.

As a simple example, let's define a class Cell that holds one element and can perform operations to put (put) or take out (get) the element. The definition of Cell is as follows.


class Cell[A](varvalue:A) {
  def put(newValue: A): Unit = {
    value = newValue
  }

  def get(): A = value
}

Let's use this in the REPL.


scala> class Cell[A](varvalue:A) {
     |   def put(newValue: A): Unit = {
     |     value = newValue
     |   }
     |   
     |   def get(): A = value
     | }
defined class Cell

scala> val cell = new Cell[Int](1)
cell: Cell[Int] = Cell@192aaffb

scala> cell.put(2)

scala> cell.get()
res1: Int = 2

scala> cell.put("something")
<console>:10: error: type mismatch;
 found   : String("something")
 required: Int
              cell.put("something")
                       ^

Of the above code


scala> val cell = new Cell[Int](1)
cell: Cell[Int] = Cell@4bcc6842

In the part of, Int type is given as a type parameter, and 1 is given as its initial value. Since I instantiated the Cell by giving the type parameter an Int, the REPL tried to put a String and the compiler rejected it as an error. Since Cell is a class that you want to instantiate by giving various types, you cannot give a specific type when defining a class. Type parameters are useful in such cases.

Next, let's look at a more practical example. The desire to return multiple values from a method is common in programming. In languages that do not have language support (which languages such as Scheme and Go have) and type parameters that return multiple values

Returns one as the return value and the other via the argument Create a class dedicated to multiple return values each time you need it There was only the option. However, the former is a bad idea in that it uses arguments as the return value, and the latter method is good if you want to return a large number of arguments or if the class has a meaningful name in the problem to be solved, but only 2 If you want to return two values, it is inconvenient because the small turn does not work. In this case, you would create a Pair class that takes two type parameters. The definition of the Pair class is as follows. Don't worry, the definition of the toString method will only be used for display later.


class Pair[A, B](vala:A,valb:B) {
  override def toString(): String = "(" + a + "," + b + ")"
}

An example of using this class Pair is the method divide, which returns both the quotient and the remainder of the division. The definition of divide is as follows.


def divide(m: Int, n: Int): Pair[Int, Int] = new Pair[Int, Int](m/n,m%n)

When these are put together in the REPL and poured, it becomes as follows.


scala> class Pair[A, B](vala:A,valb:B) {
     |   override def toString(): String = "(" + a + "," + b + ")"
     | }
defined class Pair

scala> def divide(m: Int, n: Int): Pair[Int, Int] = new Pair[Int, Int](m/n,m%n)
divide: (m: Int, n: Int)Pair[Int,Int]

scala> divide(7, 3)
res0: Pair[Int,Int] = (2,1)

You can see that the quotient of 7 divided by 3 and the remainder are in res0. In this case, new Pair [Int, Int](m / n, m% n) is used, but it can be omitted if the type of the type parameter can be inferred from the argument type. In this case, the arguments given to Pair's constructor are Int and Int, so new Pair (m / n, m% n) has the same meaning. This Pair can be used in all cases where you want to return two different types (even the same type) as a return value. In this way, the advantage of type parameters is that you can abstract the case where all types do the same thing.

By the way, since this class like Pair is often used in Scala, Tuple1 to Tuple22 (the number after Tuple is the number of elements) are prepared in advance. Also, when instantiating


scala> val m = 7
m: Int = 7

scala> val n = 3
n: Int = 3

scala> new Tuple2(m/n,m%n)
res1: (Int, Int) = (2,1)

Even if you don't


scala> val m = 7
m: Int = 7

scala> val n = 3
n: Int = 3

scala> (m/n,m%n)
res2: (Int, Int) = (2,1)

It is supposed to be good.

Displacement specification (variance)

In this section, you will learn about contravariant and covariant properties related to type parameters.

Covariant

In Scala, unspecified type parameters are usually invariant. Invariant is only when there are classes G with type parameters, type parameters A and B, and A = B


val : G[A] = G[B]

It shows the property that such substitution is allowed. This is a natural property considering that classes given different type parameters will be of different types. I dare to mention immutable here because I make a design mistake that Java's built-in array classes are covariant rather than immutable by default.

We haven't mentioned covariance yet, so let's give a brief definition. Covariation is only when there are classes G with type parameters, type parameters A and B, and A inherits from B.


val : G[B] = G[A]

It represents the property that such assignment is allowed. In Scala, when defining a class


class G[+A]

If you prefix a type parameter with +, the type parameter (or its class) will be covariant.

If this is left as it is, the definition may be abstract and difficult to understand, so I will explain using an array type as a concrete example. Array types are an interesting example in that they are covariant in Java while they are invariant in Scala. First is a Java example. Read as G = Array, A = String, B = Object.


Object[] objects = new String[1];
objects[0] = 100;

This code snippet goes through compilation as Java code. At first glance, it seems reasonable to be able to pass an array of Strings to a variable that represents an array of Objects. However, when I run this code I get the exception java.lang.ArrayStoreException. This is actually an array of Strings (which has only Strings as elements) in objects, but I'm trying to pass 100, which is an int type (boxing converted and Integer type) value in the second line. It depends.

On the other hand, in Scala, when I try to compile the code corresponding to the first line of similar code, I get the following compilation error (Any is a superclass of all types, AnyRef, and AnyVal (value type) ) Values can also be stored).


scala> val arr: Array[Any] = new Array[String](1)
<console>:7: error: type mismatch;
 found   : Array[String]
 required: Array[Any]

This is because arrays are immutable in Scala. If the type safety of a statically typed language is to catch more programming errors at compile time, then Scala is more type-safe than Java in array design.

Now, in Scala, when you covariate a type parameter, you can rest assured that the compiler will give you an error for unsafe operations, but it makes sense to know when you can use covariation. .. For example, consider the class Pair [A, B] we created earlier. Once Pair [A, B] is instantiated, it cannot be modified, so exceptions such as ArrayStoreException cannot occur. In fact, Pair [A, B] is a class that can be safely covariant, and class Pair [+ A, + B] does not cause any problems.


scala> class Pair[+A, +B](vala:A,valb:B) {
     |   override def toString(): String = "(" + a + "," + b + ")"
     | }
defined class Pair

scala> val pair: Pair[AnyRef, AnyRef] = new Pair[String, String]("foo","bar")
pair: Pair[AnyRef,AnyRef] = (foo,bar)

Here you can see that Pair cannot be changed after giving it a value at creation time, so there is no room for an exception like ArrayStoreException. In general, type parameters such as immutable, once created, can be covariant in many cases.

Contravariant

Next is the contravariance, which is exactly the opposite of the covariance. Let me give you a brief definition. A contravariant is only when there are classes G with type parameters, type parameters A and B, and A inherits from B.


val : G[A] = G[B]
It represents the property that such assignment is allowed. In Scala, when defining a class

class G[-A]

Prefixing a type parameter with-, such as, makes the type parameter (or its class) contravariant.

One of the most obvious examples of contravariance is the type of function. For example, when there are types A and B


val x1: A => AnyRef = B =>AnyRef type value
x1(Type A value)

In order for the program fragment to succeed, A must inherit from B. The opposite is not possible. Let's assume that A = String and B = AnyRef.


val x1: String => AnyRef = AnyRef =>AnyRef type value
x1(String type value)

Here, since what is actually contained in x1 is the value of AnyRef => AnyRef type, even if a String type value is given as an argument, it is the same as giving a String type value to the AnyRef type argument. It succeeds without any problems. If A and B are reversed and A = AnyRef, B = String, it will be the same as giving an AnyRef type value to the String type argument, so this will cause a compile error when assigning a value to x1. Should be, and you will actually get a compile error.

Let's actually try it with REPL.


scala> val x1: AnyRef => AnyRef = (x: String) => (x:AnyRef)
<console>:7: error: type mismatch;
 found   : String => AnyRef
 required: AnyRef => AnyRef
       val x1: AnyRef => AnyRef = (x: String) => (x:AnyRef)
                                              ^

scala> val x1: String => AnyRef = (x: AnyRef) => x
x1: String => AnyRef = <function1>

In this way, the result is as described above.

Bounds of type parameters

If you do not specify anything for the type parameter T, you only know that the type parameter T can be of any type. Therefore, the only method that can be called for the type parameter T that specifies nothing is for Any. However, it may be useful to be able to write constraints on T, for example if you want to sort a list of ordered elements. Type parameter bounds can be used in such cases. There are two types of type parameter boundaries.

Upper bounds

The first is upper bounds, which specify what types the type parameters inherit. In the upper bound, the type parameter is followed by <:, followed by the constraint type. In the following, after defining the class Show that can be converted into a string by show, ShowablePair that has only the type that is Show is defined as an element.


abstract class Show {
  def show: String
}
class ShowablePair[A <: Show, B <: Show](vala:A,valb:B) extends Show {
  override def show: String = "(" + a.show + "," + b.show + ")"
}

Here, since Show is specified as the upper bound for both type parameters A and B, show can be called for a and b. If you do not explicitly specify the upper bound, it is considered that Any is specified.

Lower bounds

The second is lower bounds, which specify what type of supertype the type parameter is. The lower bound is a feature often used with covariant parameters. Let's see an example.

First, define an immutable Stack class that was like a covariant exercise. Suppose you want this Stack to be covariant.


abstract class Stack[+A]{
  def push(element: A): Stack[A]
  def top: A
  def pop: Stack[A]
  def isEmpty: Boolean
}

However, this definition results in a compilation error similar to the following:


error: covariant type A occurs in contravariant position in type A of value element
         def push(element: A): Stack[A]
                           ^

This compilation error says that the covariant type parameter A appears at a contravariant position (where the contravariant type parameter can appear). In general, if the value of the covariant parameter E comes to the position of the argument, you will get an error like this because it can break type safety. However, unlike arrays, this Stack is immutable, so there shouldn't be any type safety issues. You can use the lower bounds of the type parameter to address this issue. Add type parameter E to push and specify Stack type parameter A as its lower bound.


abstract class Stack[+A]{
  def push[E >: A](element:E): Stack[E]
  def top: A
  def pop: Stack[A]
  def isEmpty: Boolean
}

By doing this, the compiler will know that the Stack can contain values of any supertype of A. And since the type parameter E is not covariant, it doesn't matter where it appears. In this way, the lower bound can be used to achieve both type-safe Stack and covariance.

end

Next time, I will study functions.

reference

This document is CC BY-NC-SA 3.0

image.png It is distributed under.

https://dwango.github.io/scala_text/

Recommended Posts

I touched Scala ~ [Type parameters and displacement specification] ~
I touched Scala
I touched Scala ~ [Class] ~
I touched Scala ~ [Object] ~
I touched Scala ~ [Trait] ~
I touched Scala ~ [Control syntax] ~
How should I set Firebase Analytics events and parameters?