Computer scienceProgramming languagesScalaTypes and data structuresSpecial types

Unified types

6 minutes read

In Scala, types are a fundamental aspect of the language. When writing Scala code, we constantly work with types to define variables, parameters, return types, and expressions. Types in Scala provide compile-time guarantees about the structure and behavior of our code, enabling static type checking and preventing certain types of errors. While the concept of types is straightforward when dealing with classes and their inheritance architecture, Scala offers a rich set of other types. These types allow us to express various abstractions and handle different kinds of values in a type-safe manner.

Architecture of types

In Scala, types are organized in a hierarchical architecture that defines their relationships and interdependencies. At the top of this hierarchy is the root type called Any. It serves as the supertype for all other types in Scala.

val list: List[Any] = List(
  123,  // an integer
  'r',  // a character
  true, // a boolean value
  "Scala",
  myClassObj // My class definition
)

Any has two direct subclasses that form the main branches of the type hierarchy: AnyVal and AnyRef. AnyVal represents the supertype of all value types in Scala. Value types are the counterparts of primitive types in Java and include types like Int, Boolean, Double, etc. They are treated as objects and can be assigned to variables of type Any, AnyVal, or their specific type.

val list: List[AnyVal] = List(
  123,
  'r', 
  true,
) // without reference types

In this code example, we can't use the reference type as a user-defined class and String in AnyVal.

AnyRef stands as the supertype of all reference types in Scala. This category encompasses class types, interface types, and user-defined types, serving as the equivalent of class types in Java. In Scala, every class or interface implicitly extends AnyRef, similar to how all classes in Java implicitly extend java.lang.Object.

val myObj = Object()
val list: List[AnyRef] = List(
   "Scala",
   myObj // Object
)

The final piece in the entire type architecture is the Nothing type. In Scala, Nothing is a special type that acts as a subtype of all other types. It is commonly referred to as the 'bottom type" because it resides at the lowest point in the type hierarchy. Note that there are no values that can have the type Nothing. It is primarily utilized to indicate exceptional or abnormal situations, such as the non-termination or the absence of a return value. Let's take a closer look at its usage.

Uses of the Nothing type

The Nothing type has some peculiarities when used in return values, particularly in the context of the Option[A] implementation.

sealed trait Option[A]

case class Some[A](value: A) extends Option[A]
case object None extends Option[Nothing]

In the above code snippet, Option is a data type in Scala used to represent optional values. It has two subtypes: Some[A], which represents a value of type A that is present, and None, which represents the absence of a value.

Note that None is an instance of Option[Nothing]. This is possible because Nothing is a subtype of every other type, including A in Option[A]. By using Nothing as the type parameter for None, it can be considered compatible with any Option type.

The use of Nothing as the type parameter for None allows for more concise and flexible code. For example, when using pattern matching with Option, you can have a single case for handling the absence of a value:

anOption match
  case Some(value) => doActionOn(value)
  case None => println("No value")

New features of Scala 3

Scala 3 has new features working with unified types. Let's get to know them better.

  • Union types: Scala 3 introduces union types which allow you to express that a value can have multiple possible types. Union types are denoted using the | operator. For example:
val a: Boolean | Int = 123

Boolean | Int represents a value that can be either a Boolean or an Int. Also, you can separate types with a case method:

case class Username(name: String)
case class Email(mail: String)

def getIdentificator(id: Username | Email) =
  id match
    case Username(name) => name
    case Email(email) => email
  • Intersection types: similar to union types, Scala 3 introduces intersection types, which allow you to express that a value must have all the specified types. Intersection types are denoted using the & operator. These types enable more precise type constraints and can be useful in scenarios where multiple type constraints must be satisfied.
trait CanFly:
  def fly(): Unit

trait CanSwim:
  def swim(): Unit

def getOnlyFlySwimBirds(bird: CanFly & CanSwim): Unit =
  bird.fly()
  bird.swim()

class Duck extends CanSwim with CanFly // will satisfy the function
class Penguin extends CanSwim // won't satisfy
  • Match types: Scala 3 introduces match types, which enable type-level pattern matching. Match types allow you to define type-level functions that operate on types and perform pattern matching on their structures.
type Bigger[T] = T match {
  case Float => Double
  case Int => Long
  case Long => BigInt
}

val floatResult: Bigger[Float] = 0.10 // is Double
val intResult: Bigger[Int] = 3_000_000_000L // is Long

The code compiles successfully because the match types correctly infer the return types based on the input types specified in the type aliases.

These are just a few highlights of the new features and improvements in Scala 3 related to types.

Conclusion

Great! Now you have a solid understanding of the types' architecture in Scala and the utilization of the special type Nothing. You have also learned how Nothing is employed within the Option[A] construct. Additionally, you have been introduced to some exciting features of Scala 3. With this knowledge, you are well-equipped to use the power of types and explore the enhanced capabilities offered by Scala 3.

How did you like the theory?
Report a typo