CSC 347 - Concepts of Programming Languages

Algebraic Data Types

Instructor: Stefan Mitsch

Pattern Matching with Complex Datatypes

  • Recursively traverse a data structure
  1. abstract class Node
  2. class Val(val x: Int) extends Node
  3. class Op(val l: Node, val r: Node, val f: (Int, Int)=>Int) extends Node
  4. def eval(node: Node) : Int = node match
  5. case n: Val => n.x
  6. case n: Op =>
  7. val l = eval(n.l)
  8. val r = eval(n.r)
  9. f(l, r)
  • Why do we not have support to decompose objects into their components?
    1. def eval(node: Node) : Int = node match
    2. case Val(x) => x
    3. case Op(l, r) => f(eval(l), eval(r))

Learning Objectives

How do we represent complex user-defined data types that support pattern matching?

  • Understand algebraic data types
  • Understand Scala enum classes

Algebraic Data Types

  • Product types: combine multiple data elements into one unit (tuples and classes)
  • Sum types: distinguish between multiple alternatives (discriminated union, variants, subclasses)
  • Algebraic data types: sum of products
  • Decompose values of algebraic data types with pattern matching

Algebraic Data Types

Product Types

  • Named for Cartesian product of sets
  • Case class definition for product of Int and String
    1. case class C (x:Int, y:String)
  • new unnecessary for constructing instances
    1. val c:C = C (5, "hello")
  • Extract elements with pattern matching
    1. val n:Int = c match
    2. case C (a, b) => a

Scala Case Classes

  • Compiler treatment for case classes
  • Class parameters are visible and immutable (val)
    1. case class C (x:Int, y:String)
    2. val c:C = C (5, "hello")
    3. val a:Int = c.x
    4. c.x = 6 // error: reassignment to val
  • Sensible toString implementation
  • Companion object with apply method
    • used to construct instances
  • Pattern matching support
    • see unapply method / extractors in textbook

Set Union

  • Cartesian product of sets
  • Union of sets
  • Coproduct or disjoint union of sets
  • Elements are tagged to indicate their source

Disjoint Union: Scala Enum

  • Disjoint union of a value and a binary operator
  • Scala enum similar to Java interface
  1. enum Node:
  2. case Val(val x: Int)
  3. case Op(val l: Node, r: Node, val f: (Int, Int)=>Int)
  • Definition can be recursive
  • Create instances
    1. val three = Val(3)
    2. val op = Op(three, Val(5), _ + _)

Disjoint Union: Scala Enum

  • Pattern match to decompose
    1. def eval(node: Node) : Int = node match
    2. case Val(x) => x
    3. case Op(l, r) => f(eval(l), eval(r))
    4. // (3+5)*2
    5. eval(Op(Op(Val(3), Val(5), _ + _), Val(2), _ * _))

Disjoint Union: C

  • Union types in C
  1. struct s_val_t;
  2. struct s_op_t;
  3. union u_node_t {
  4. struct s_val_t* u_val;
  5. struct s_op_t* u_op;
  6. };
  7. struct s_val_t { int x; };
  8. struct s_op_t {
  9. struct u_node_t l;
  10. struct u_node_t r;
  11. int (*op)(int,int);
  12. };
  • What is stored in u_node_t?
  • ... must be tagged manually
    1. enum e_node_t {
    2. e_val,
    3. e_op,
    4. };
    5. struct node_t {
    6. enum e_node_t tag;
    7. union u_node_t content;
    8. };
    9. struct s_op_t {
    10. struct node_t l;
    11. struct node_t r;
    12. int (*op)(int,int);
    13. };

Disjoint Union: C

  • Create instances: tag / union selector must match!
    1. int add(int x, int y) { return x+y; }
    2. struct s_val_t three = { .x=3 };
    3. struct s_val_t five = { .x=5 };
    4. struct s_op_t op = {
    5. .l = {
    6. .tag = e_val,
    7. .content = { .u_val = &three }
    8. },
    9. .r = {
    10. .tag = e_val,
    11. .content = { .u_val = &five }
    12. },
    13. .op = &add
    14. };
    15. struct node_t node = {
    16. .tag = e_op,
    17. .content = { .u_op = &op }
    18. };

Disjoint Union: C

  • Examine tag to decompose: only access union selector matching tag!
    1. int eval (struct node_t* node) {
    2. switch (node->tag) {
    3. case e_val:
    4. return node->content.u_val->x;
    5. case e_op: {
    6. struct s_op_t* o = node->content.u_op;
    7. return o->op(eval(&o->l), eval(&o->r));
    8. }
    9. default:
    10. fprintf (stderr, "Unknown tag\n");
    11. exit (1);
    12. }
    13. }

Recursive Types

  • Classes can be recursive
  • Peano natural numbers: either or a transitive successor of it
  • Algebraic data type PeanoNat
    1. enum PeanoNat:
    2. case Zero
    3. case Succ (n:PeanoNat)
  • Define functions between PeanoNat and Int
    1. def peano2int (p:PeanoNat, result: Int = 0): Int = p match
    2. case PeanoNat.Zero => result
    3. case PeanoNat.Succ(n) => peano2int (n, result+1)
    4. import PeanoNat.*
    5. val q = Succ (Succ (Succ (Zero))) // : Peano = ...
    6. peano2int (q) // : Int = 3

Exercise: Linked List

Which case classes and case objects?

  • An Empty list and a Cons cell of at least one element
    1. enum MyList:
    2. case Empty
    3. case Cons (head:Int, tail:MyList)

Exercise: Linked List

  1. enum MyList:
  2. case Empty
  3. case Cons (head:Int, tail:MyList)

Create an empty list?

  • Simply use Empty

    1. import MyList.*
    2. val xs = Empty
  • Create an instance of a list?

  • Nest Cons and terminate with Empty

    1. import MyList.*
    2. val xs = Cons (1, Cons(2, Cons(3, Empty)))

Exercise: Linked List

  1. enum MyList:
  2. case Empty
  3. case Cons (head:Int, tail:MyList)
  4. object MyList:
  5. def create(elems: Int*) =
  6. elems.foldRight(Empty)((e, l) => Cons(e, l))
  • Create an instance of a list?

  • Use method create

    1. import MyList.*
    2. // val xs = Cons (1, Cons(2, Cons(3, Empty)))
    3. val xs = MyList.create(1, 2, 3)

Exercise: Linked List

Generalize to any element type?

  1. enum MyList[+X]:
  2. case Empty
  3. case Cons (head:X, tail:MyList[X])

Compute the length of such a list?

  • Recursive with pattern matching
    1. def length [X] (xs:MyList[X]): Int = xs match
    2. case MyList.Empty => 0
    3. case MyList.Cons(a,as) => 1 + length(as)
  • Tail-recursive with pattern matching
    1. def length [X] (xs:MyList[X], result:Int = 0): Int = xs match
    2. case MyList.Empty => result
    3. case MyList.Cons(a,as) => length(as, result+1)

Exercise: Binary Tree

  • Data stored at leaves
  • Operations stored at internal nodes
  • Internal nodes have left and right subtrees
    1. enum Tree[X]:
    2. case Leaf (data:X)
    3. case Node (l:Tree[X], f:(X,X)=>X, r:Tree[X])
  • Fold tree into result by applying all the intermediate operations

  • Recursive with pattern matching

    1. def fold [X] (t: Tree[X]) : X = t match
    2. case Leaf(x) => x
    3. case Node(l, f, r) => f(fold(l), fold(r))

Summary

  • Algebraic data types: Sums of products
  • In Scala: enum and case classes