-
Martin Odersky authoredMartin Odersky authored
- Enums: Motivation
- Enums
- Enumerate the Values of an Enumeration
- Discriminate the Values of an Enumeration
- Match Syntax
- Enumerations Can Have Methods
- Enumerations Values Can Take Parameters
- Matching on Constructors
- Evaluating Match Expressions
- Forms of Patterns
- What Do Patterns Match?
- Matching on Case Classes
- Relationship Between Case Classes and Parameterized Enum Cases
- Enumerations Can Be Recursive
- Enumerations Can Take Parameters
- Exercise: Arithmetic Expressions
- Expr Type Definition
% 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
andParadigm.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.