Skip to content
Snippets Groups Projects
Forked from an inaccessible project.

% Enums % %

Enums: Motivation

We have seen that case classes aggregate several values into a single abstraction. For instance, the Rational case class aggregates a numerator and a denominator.

Conversely, how could we define an abstraction accepting alternative values?

\example

Define a Paradigm type that can be either Functional or Imperative.

Enums

We can define an abstraction accepting alternative values with an \red{enum}:

enum Paradigm
  case Functional, Imperative

This definition introduces:

  • A new \red{type}, named Paradigm.
  • Two possible \red{values} for this type, Paradigm.Functional and Paradigm.Imperative.

Enumerate the Values of an Enumeration

It is possible to enumerate all the values of an enum by calling the values operation on the enum companion object:

\begin{tabular}{ll} \verb@Paradigm.values@ \wsf Array(Functional, Imperative) \ \verb@val p = Paradigm.Functional@ \wsf p: Paradigm = Functional \ \verb@p == Paradigm.values(0)@ \wsf true \end{tabular}

Discriminate the Values of an Enumeration

You can discriminate between the values of an enum by using a \red{match} expression:

def isReferentiallyTransparent(paradigm: Paradigm): Boolean =
  paradigm match
    case Paradigm.Functional => true
    case Paradigm.Imperative => false

->

And then:

\begin{tabular}{ll} \verb@val p = Paradigm.Functional@ \wsf p: Paradigm = Functional \ \verb@isReferentiallyTransparent(p)@ \wsf true \end{tabular}

Match Syntax

Pattern matching is a generalization of switch from C/Java to class hierarchies.

It’s expressed in Scala using the match keyword.

  • match is followed by a sequence of \red{cases}, case value => expr.
  • Each case associates an \red{expression} expr with a \red{constant} value.

We will see in the next slides that pattern matching can do more than discriminating enums.

Enumerations Can Have Methods

Alternatively, we can define isReferentiallyTransparent as a method:

enum Paradigm
  case Functional, Imperative

  def isReferentiallyTransparent: Boolean = this match
    case Functional => true
    case Imperative => false

->

And then:

\begin{tabular}{ll} \verb@val p = Paradigm.Functional@ \wsf p: Paradigm = Functional \ \verb@p.isReferentiallyTransparent@ \wsf true \end{tabular}

Enumerations Values Can Take Parameters

Consider the following definition of a data type modeling students following a lecture:

enum Student
  case Listening
  case Asking(question: String)

This definition introduces a Student type consisting of two cases, Listening or Asking. The Asking case is parameterized with a value parameter question. Since Listening is not parameterized, it is treated as a normal enum value.

Matching on Constructors

Since Asking is not a constant, we match on it using a constructor pattern:

student match
  case Student.Listening => "Student is listening"
  case Student.Asking(q) => s"Student is asking: $q"

A constructor pattern allows us to extract the value of the question parameter, in case the student value is indeed of type Asking.

Here, the q identifier is bound to the question parameter of the student object.

Evaluating Match Expressions

An expression of the form \btt e\ match\ \{\ case\ p_1 => e_1\ ...\ case\ p_n => e_n\ \} matches the value of the selector \btt e with the patterns \btt p_1, ..., p_n in the order in which they are written.

The whole match expression is rewritten to the right-hand side of the first case where the pattern matches the selector e.

References to pattern variables are replaced by the corresponding parts in the selector.

Forms of Patterns

Patterns are constructed from:

  • constructors, e.g. Rational, Student.Asking,
  • variables, e.g. n, e1, e2,
  • wildcard patterns _,
  • constants, e.g. 1, true, Paradigm.Functional.

Variables always begin with a lowercase letter.

The same variable name can only appear once in a pattern.

Names of constants begin with a capital letter, with the exception of the reserved words null, true, false.

What Do Patterns Match?

  • A constructor pattern \btt C(p_1 , ..., p_n) matches all the values of type \btt C (or a subtype) that have been constructed with arguments matching the patterns \btt p_1, ..., p_n.
  • A variable pattern \btt x matches any value, and \red{binds} the name of the variable to this value.
  • A constant pattern \btt c matches values that are equal to \btt c (in the sense of ==)

Matching on Case Classes

Constructor patterns also works with case classes:

def invert(x: Rational): Rational =
  x match
    case Rational(0, _) => sys.error("Unable to invert zero")
    case Rational(n, d) => Rational(d, n)

Relationship Between Case Classes and Parameterized Enum Cases

Case classes and parameterized enum cases are very similar.

When should you use one or the other?

  • Parameterized enum cases should be used to define a type that is part of a set of alternatives,
  • If there are no alternatives, just use a case class.

Enumerations Can Be Recursive

A list of integer values can be modeled as either an empty list, or a node containing both a number and a reference to the remainder of the list.

enum List
  case Empty
  case Node(value: Int, next: List)

A list with values 1, 2, and 3 can be constructed as follows:

List.Node(1, List.Node(2, List.Node(3, List.Empty)))

Enumerations Can Take Parameters

enum Vehicle(val numberOfWheels: Int)
  case Unicycle extends Vehicle(1)
  case Bicycle  extends Vehicle(2)
  case Car      extends Vehicle(4)
  • Enumeration cases have to use an explicit extends clause

Exercise: Arithmetic Expressions

Define a datatype modeling arithmetic expressions.

An expression can be:

  • a number (e.g. 42),
  • a sum of two expressions,
  • or, the product of two expressions.

Expr Type Definition

enum Expr
  case Number(n: Int)
  case Sum(lhs: Expr, rhs: Expr)
  case Prod(lhs: Expr, rhs: Expr)

->

\begin{tabular}{ll}
 \verb@val one = Expr.Number(1)@  \wsf one: Expr = Number(1) \\
 \verb@val two = Expr.Number(2)@  \wsf two: Expr = Number(2) \\
 \verb@Expr.Sum(one, two)@        \wsf Sum(Number(1), Number(2))
\end{tabular}


Exercise
========

Is the following match expression valid?

expr match case Expr.Sum(x, x) => Expr.Prod(2, x) case e => e


     O         Yes
     O         No

Exercise
========

Implement an `eval` operation that takes an `Expr` as parameter and
evaluates its value:

def eval(e: Expr): Int


Examples of use:

\begin{tabular}{ll}
 \verb@eval(Expr.Number(42))@  \wsf 42 \\
 \verb@eval(Expr.Prod(2, Expr.Sum(Expr.Number(8), Expr.Number(13))))@  \wsf 42
\end{tabular}

Exercise
========

Implement a `show` operation that takes an `Expr` as parameter and
returns a `String` representation of the operation.

Be careful to introduce parenthesis when necessary!

def show(e: Expr): String


Examples of use:

\begin{tabular}{ll}
 \verb@show(Expr.Number(42))@  \wsf "42" \\
 \verb@show(Expr.Prod(2, Expr.Sum(Expr.Number(8), Expr.Number(13))))@  \wsf "2 * (8 + 13)"
\end{tabular}

Summary
=======

In this lecture, we have seen:

- how to define data types accepting alternative values using
  `enum` definitions,
- enumeration cases can be discriminated using pattern matching,
- enumeration cases can take parameters or be simple values,
- pattern matching allows us to extract information carried by an object.