Scala - Algebraic Data Types (ADTs)



Algebraic Data Types (ADTs) are used in functional programming for nested and complex data structures in a type-safe manner. There are categories of Algebraic Data Types (ADTs): product types and sum types.

1. Product Types

Product types combine multiple values into a single value. These are called "product types". Because these represent Cartesian products of their component types. For example, case classes in Scala are used to define product types.

Example

Following is the example which shows you how to product type using case class -

case class Person(name: String, age: Int)

object Demo {
  def main(args: Array[String]): Unit = {
    // The compiler infers the type as Person
    val person = Person("Zara", 25) 

    println(person)
  }
}

Save the above code in Demo.scala. The following commands are used to compile and execute this program.

Command

> scalac Demo.scala
> scala Demo

Output

The above code defines Person case class with name and age fields. The compiler infers its type and initializes the fields when you create instance of Person.

This will produce the following result -

Person(Zara,25)

2. Sum Types

Sum types represent a value that can be one of various different types. These are called "sum types". Because these represent the sum of their component types. For example, sealed traits along with case classes (or case objects) are used to define sum types in Scala.

Sum types are also known as tagged (or disjoint) unions.

Example

Following is the example which shows you how to sum type using sealed trait and case classes -

sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape

object Demo {
  def main(args: Array[String]): Unit = {
    val shape1: Shape = Circle(5.0)
    val shape2: Shape = Rectangle(4.0, 6.0)

    def describeShape(shape: Shape): String = shape match {
      case Circle(radius) => s"Circle with radius $radius"
      case Rectangle(width, height) => s"Rectangle with width $width and height $height"
    }

    println(describeShape(shape1))
    println(describeShape(shape2))
  }
}

Save the above code in Demo.scala. The following commands are used to compile and execute this program.

Command

> scalac Demo.scala
> scala Demo

Output

The above code defines sum type Shape with two possible cases: Circle and Rectangle. The describeShape function uses pattern matching to handle different shapes.

This will produce the following result -

Circle with radius 5.0
Rectangle with width 4.0 and height 6.0

Combining Product and Sum Types

You can combine product and sum types to model more complex and nested data structures.

Example

Following is the example which shows you how to combine product and sum types -

sealed trait Vehicle
case class Car(make: String, model: String) extends Vehicle
case class Bike(make: String, engineCapacity: Int) extends Vehicle

case class Owner(name: String, vehicle: Vehicle)

object Demo {
  def main(args: Array[String]): Unit = {
    val owner1 = Owner("Nuha", Car("Toyota", "Camry"))
    val owner2 = Owner("Ayan", Bike("Yamaha", 600))

    def describeOwner(owner: Owner): String = owner.vehicle match {
      case Car(make, model) => s"${owner.name} owns a car: $make $model"
      case Bike(make, engineCapacity) => s"${owner.name} owns a bike: $make with engine capacity $engineCapacity cc"
    }

    println(describeOwner(owner1))
    println(describeOwner(owner2))
  }
}

Command

> scalac Demo.scala
> scala Demo

Output

The above code combines product and sum types to define Owner with Vehicle that can be either a Car or a Bike -

This will produce the following result -

Nuha owns a car: Toyota Camry
Ayan owns a bike: Yamaha with engine capacity 600 cc

Recursive Algebraic Data Types

Algebraic Data Types (ADTs) can also be recursive. So you can define complex and nested data structures.

Example

Following is the example which shows you how to recursive ADT representing binary tree -

sealed trait Tree[+A]
case class Leaf[A](value: A) extends Tree[A]
case class Node[A](left: Tree[A], right: Tree[A]) extends Tree[A]

object Demo {
  def main(args: Array[String]): Unit = {
    def sumTree(tree: Tree[Int]): Int = tree match {
      case Leaf(value) => value
      case Node(left, right) => sumTree(left) + sumTree(right)
    }

    val tree: Tree[Int] = Node(Leaf(1), Node(Leaf(2), Leaf(3)))
    val result = sumTree(tree)
    println(result)
  }
}

Save the above code in Demo.scala. The following commands are used to compile and execute this program.

Command

> scalac Demo.scala
> scala Demo

Output

The above code defines recursive binary tree ADT and function to sum the values of the tree.

This will produce the following result -

6

Pattern Matching with ADTs

Pattern matching is a feature in Scala. It works with Algebraic Data Types (ADTs) to handle different cases.

Example

Following is the example which shows you how to use pattern matching with an ADT -

Save the above code in Demo.scala. The following commands are used to compile and execute this program.

sealed trait Result[+A]
case class Success[A](value: A) extends Result[A]
case class Failure(message: String) extends Result[Nothing]

object Demo {
  def main(args: Array[String]): Unit = {
    def processResult(result: Result[Int]): String = result match {
      case Success(value) => s"Success with value: $value"
      case Failure(message) => s"Failure with message: $message"
    }

    val successResult: Result[Int] = Success(42)
    val failureResult: Result[Int] = Failure("Error occurred")

    println(processResult(successResult))
    println(processResult(failureResult))
  }
}

Save the above code in Demo.scala. The following commands are used to compile and execute this program.

Command

> scalac Demo.scala
> scala Demo

Output

The above code defines generic sum type Result with two cases: Success and Failure. The processResult function uses pattern matching to handle different results.

This will produce the following result -

Success with value: 42
Failure with message: Error occurred

Notes

  • Algebraic Data Types (ADTs) combine types to create more complex types.
  • Product types combine multiple values into a single value (e.g., case classes).
  • Sum types represent a value that can be one of several different types (e.g., sealed traits and case classes).
  • ADTs can be recursive. So you can define complex and nested data structures.
  • You can combine product and sum types for nested data structures in a type-safe way.
  • ADTs reduce the need for explicit type annotations.
  • Recursive ADTs can represent hierarchical and nested data, like trees and graphs.
Advertisements