Generic types allows us to abstract over types. If we don’t care what type is stored but we want to make sure we preserve the type when we get the value out, then use a generic type. Generic classes or traits takes a type parameter within a square bracket [ ]. The Scala convention is to use a single letter like A to name those parameters.
final case class Box[A](value: A)
Box(2)
// val res0: Box[Int] = Box(2)
res0.value
// val res1: Int = 2
The syntax [A] is called a type parameter. Type parameters can also be added to methods to limit the scope of the parameter
def generic[A](in: A):A = in
generic[String]("Hello")
// val res4: String = Hello
Invariant Generic Sum Type Pattern
If A of type T is a B or C:
sealed trait A[T]
final case class B[T]() extends A[T]
final case class C[T]() extends A[T]
Now, let’s look into function types. To declare a function type
(A, B, ...) => C
Where:
- A, B, ... are the types of the input parameters
- C is the type of the result.
For example: if we want a function that accepts two Int and returns an Int, we can write the function type as (Int, Int) ⇒ Int
Scala also gives us a function literal syntax specifically for creating new functions. Syntax for creating a function literal
(parameter: type, ...) => expression
Where
- the optional parameters are the names given to the function parameters
- the types are the types of the function parameters
- expression determines the result of the function
Example:
val add = (x: Int) => x + 1
We can use generics to model Product Types. Consider a method that returns an Int and as String.
def intAndString: ??? = // ...
We can use generics to create a product type. For example - a Pair that returns the relevant data for both return types.
def intAndString: Pair[Int, String] = // ...
Scala provides tuple to create a pair of data. In Scala, we can create tuple of up to 22 elements. The classes are called Tuple1[A] through to Tuple22[A, B, C, …]
Tuple3("Hello", 1, 2.3)
// val res5: (String, Int, Double) = (Hello,1,2.3)
Sequencing Computation
Let’s suppose
- we have type G[A] and a function A ⇒ B and we want a result G[B]. The method that performs this operation is map.
sealed trait LinkedList[A] {
def map[B](fn: A => B): LinkedList[B] = this match {
case End() => End[B]()
case Pair(hd, tl) => Pair(fn(hd), tl.map(fn))
}
}
case class Pair[A](hd: A, tl: LinkedList[A]) extends LinkedList[A]
case class End[A]() extends LinkedList[A]
- we have type G[A] and a function A ⇒ G[B], and we want a result G[B]. The method that performs this operations is called flatMap.
sealed trait Maybe[A] {
def flatMap[B](fn: A => Maybe[B]): Maybe[B] =
this match {
case Full(v) => fn(v)
case Empty() => Empty[B]()
}
}
final case class Full[A](value: A) extends Maybe[A]
final case class Empty[A]() extends Maybe[A]
Variance
If we have some type Foo[A], and A is a subtype of B, is Foo[A] a subtype of Foo[B]? The answer depends on the variance of the type Foo.
- A type Foo[T] is invariant in terms of T, meaning that the types of Foo[A] and Foo[B] are unrelated regardless of the relationship between A and B. This is the default variance of any generic type in Scala.
- A type Foo[+T] is covariant in terms of T, meaning that Foo[A] is supertype of Foo[B] if A is a supertype of B. Most Scala collections are covariant in terms of their contents.
- A type Foo[-T] is contravariant in terms of T, meaning that Foo[A] is a subtype of Foo[B] if A is a supertype of B.
Covariant Generic Sum Type Pattern
If A is of type T is a B or C, and C is not generic, write
sealed trait A[+T]
final case class B[T](t: T) extends A[T]
case object C extends A[Nothing]
Contravariant Position Pattern
If A of a covariant type T and a method f of A complains that T is used in a contravariant position, introduce a type TT >: T in f.
case class A[+T]() {
def f[TT >: T](t: TT): A[TT] = ???
}
Type Bounds
Use A <: B to declare A must be a subtype of B
Use A >: B to declare A must be a supertype of B
References used are scala doc and essential scala