CSC447

Concepts of Programming Languages

Algebraic Data Types

and Pattern Matching

Instructor: James Riely

Video

Open on youtube

Algebraic Data Types

  • Product types: tuples
  • Sum types: discriminated/tagged union, variants
  • Algebraic data types: sum of products
  • Examples seen so far
    • Option type
    • List type
  • Decompose values of algebraic data types with pattern matching

Algebraic Data Types

Product Types

  • Named for Cartesian product of sets
    • X * Y = { (x, y) | x ∈ X ∧ y ∈ Y }
  • Case class definition for product of Int and String
    
    case class C (x:Int, y:String)
                  
  • new unnecessary for constructing instances
    
    val c:C = C (5, "hello")
                  
  • Extract elements with pattern matching
    
    val n:Int = c match  
      case C (a, b) => a
                  

Case Classes

  • Compiler treatment for case classes
  • Constructor arguments are visible and immutable
    
    case class C (x:Int, y:String)
    val c:C = C (5, "hello")
    val a:Int = c.x
    c.x = 6 // error: reassignment to val
                  
  • Generate sensible toString implementation
  • Generate companion object with apply method
    • used to construct instances
  • Generate pattern matching support
    • see unapply method / extractors in textbook

Tuples are Case Classes

  • Pairs / tuples are syntactic sugar for case classes
  • See Tuple3.scala source
    
    case class Tuple3[+T1, +T2, +T3](_1: T1, _2: T2, _3: T3)
    extends Product3[T1, T2, T3]:
      override def toString() = "(" + _1 + "," + _2 + "," + _3 + ")"
                  
  • Examine runtime type without syntactic sugar
    
    scala> (5, "hello", true).getClass
    res0: Class[_ <: (Int, String, Boolean)] = class scala.Tuple3
                  

Set Union

  • Cartesian product of sets
    • X * Y = { (x, y) | x ∈ X ∧ y ∈ Y }
  • Union of sets
    • X ∪ Y = { z | z ∈ X ∨ z ∈ Y }
  • Coproduct or disjoint union of sets
    • X ⊕ Y = X ⊍ Y = { (0, x) | x ∈ X } ∪ { (1, y) | y ∈ Y }
  • Elements are tagged to indicate their source

Disjoint Union - Case Classes

  • Disjoint union of 3 ints and 1 int
  • trait similar to Java interface

enum DateSpecifier:
  case Absolute (year:Int,mon:Int,day:Int)
  case Relative (daysOffset:Int)
          

Disjoint Union - Case Classes

  • Create instances

val ds = new Array[DateSpecifier] (2)
ds (0) = DateSpecifier.Absolute (2022, 11, 1)
ds (1) = DateSpecifier.Relative (-5)
          

Disjoint Union - Case Classes

  • Pattern match to decompose

import DateSpecifier.*
def resolveDate (d:DateSpecifier) : String = 
  d match 
    case Absolute (y, m, d) => "%04d-%02d-%02d".format(y, m, d)
    case Relative (o)       => java.time.LocalDate.now.nn.plusDays(o).toString
ds map resolveDate		    
          

Disjoint Union - C

  • Union types in C must be tagged manually

struct s_absolute_t {
  int year;
  int mon;
  int day;
};

struct s_relative_t {
  int days_offset;
};

union u_ds_t {
  struct s_absolute_t u_absolute;
  struct s_relative_t u_relative;
};

enum e_ds_t {
  e_absolute,
  e_relative,
};

struct ds_t {
  enum e_ds_t tag;
  union u_ds_t content;
};
          

Disjoint Union - C

  • Create instances
  • tag / union selector must match!

struct ds_t ds[2];
ds[0].tag = e_absolute;
ds[0].content.u_absolute.year = 2030;
ds[0].content.u_absolute.mon  = 0;
ds[0].content.u_absolute.day  = 1;
ds[1].tag = e_relative; 
ds[1].content.u_relative.days_offset = -5;
          

Disjoint Union - C

  • Examine tag to decompose
  • Only access union selector matching tag!

void print_ds (struct ds_t *dsp) {
  switch (dsp->tag) {
  case e_absolute:
    printf ("absolute (%d, %d, %d)\n", dsp->content.u_absolute.year, 
                                       dsp->content.u_absolute.mon, 
                                       dsp->content.u_absolute.day);
    break;
  case e_relative:
    printf ("relative (%d)\n", dsp->content.u_relative.days_offset);
    break;
  default:
    fprintf (stderr, "Unknown tag\n");
    exit (1);
  }
}
          

Recursive Types

  • Classes can be recursive
  • Peano natural numbers
  • Define functions between PeanoNat and Int

enum Peano:
  case Zero
  case Succ (n:Peano)

def peano2int (p:Peano): Int = p match 
  case Peano.Zero    => 0
  case Peano.Succ(n) => 1 + peano2int (n)

import Peano.*
val q = Succ (Succ (Succ (Zero))) // : Peano = ...
peano2int (q) // : Int = 3
          

Linked Lists

  • Case object/class for Empty and Cons
  • Empty used at any List type

enum MyList[+X]:
  case Empty
  case Cons (head:X, tail:MyList[X])

def length [X] (xs:MyList[X]): Int = xs match 
  case MyList.Empty => 0
  case MyList.Cons(a,as) => 1 + length(as)

import MyList.*
val xs = Cons (11, Cons(21, Cons(31, Empty))) // : MyList[Int] = ...
length (xs)    // : Int = 3
val ys = Empty // : MyList[Nothing] = ...
          

Binary Trees

  • Binary trees with
    • no data stored at leaves
    • elements stored at internal nodes
    • internal nodes have left and right subtrees

enum Tree[+X]:
  case Leaf
  case Node (l:Tree[X], c:X, r:Tree[X])
          

Red-Black Trees


enum Color { case Red, Black }
enum RBTree[+K,+V]:
  case Leaf
  case Node (k:K, v:V, c:Color, l:RBTree[K,V], r:RBTree[K,V]) 

import RBTree.*;
def rotateLeft  [K,V] (t:Node[K,V]) : Node[K,V] = 
  t match 
    case Node (k1, v1, c, l, Node (k2, v2, Color.Red, m, r)) =>
      Node (k2, v2, c, Node (k1, v1, Color.Red, l, m), r)
    case _ => 
      throw new RuntimeException ("does not match: " + t)