(13) Functional Type Handling

(13) Functional Type Handling

About

:octocat: GitHub: All of the example code: repo (link)

:page_facing_up: blog link: https://purrgramming.life/cs/programming/fp/ :star:


Type Erasure

Definition

Scala: statically typed programming, i.e. The compiler determines the type of a variable at compile time.

Type erasure is the load-time process by which explicit type annotations are removed from a program before execution at run-time.

Operational semantics that does not require programs to be accompanied by types are type-erasure semantics, contrasted with type-passing semantics.

Why

After compilation, the compiler removes all generic type information, as they don’t know about generics.

Example

i.e. Map[Int, Int] and Map[String, String] are the same at runtime.


def isIntIntMap(x: Any) = x match {

case m: Map[Int, Int] => true

case _ => false

}

Result


//val res0: Boolean = true

isIntIntMap(Map(1 -> 1))

//val res1: Boolean = true

isIntIntMap(Map("abc" -> "abc"))

Warning

We would get Unchecked Warning for #isIntIntMap


def isIntIntMap(x: Any): Boolean

-- Unchecked Warning:

2 | case m: Map[Int, Int] => true

| ^

| the type test for Map[Int, Int] cannot be checked at runtime

Exception

Exception – arrays, because they are handled specially in Java and Scala.

The element type of an array is stored with the array value to pattern match on it.


//-----------------------------------

// Exception

def isStringArray(x: Any) = x match {

case a: Array[String] => "yes"

case _ => "no"

}

//val res2: String = yes

isStringArray(Array("abc"))

//val res3: String = no

isStringArray(Array(1, 2, 3))

Decomposition

Issue to Solve

We are trying to find a general and convenient way to access heterogeneous data in a class hierarchy.

Example:

An interpreter for arithmetic expressions for numbers and additions.

Non-Solution 1: Classification and access methods

Expressions can be represented as a class hierarchy, with a base trait Expr

and two subclasses, Number and Sum.

The expression’s shape and its components:


trait Expr {

def isNumber: Boolean

def isSum: Boolean

def numValue: Int

def leftOp: Expr

def rightOp: Expr

}

class Number(n: Int) extends Expr {

def isNumber = true

def isSum = false

def numValue = n

def leftOp = throw Error("Number.leftOp")

def rightOp = throw Error("Number.rightOp")

}

class Sum(e1: Expr, e2: Expr) extends Expr {

def isNumber = false

def isSum = true

def numValue = throw Error("Sum.numValue")

def leftOp = e1

def rightOp = e2

}

Problem: Quadratic explosion**

  • Writing all these classification and accessor functions quickly becomes tedious

  • There’s no static guarantee you use the right accessor functions. You might hit an Error case if you

are not careful

Non-Solution 2: Type Tests and Type Casts

Note: Type tests and type casts are discouraged in Scala –

A “hacky” solution could use type tests and type casts. Scala lets you do these using methods defined in class Any:


// checks whether this object’s type conforms to ‘T‘

def isInstanceOf[T]: Boolean

// treats this object as an instance of type ‘T‘

def asInstanceOf[T]: T

// throws ‘ClassCastException‘ if it isn’t.

like


(1).isInstanceOf[Int] // true

(1).asInstanceOf[Double] // val res1: Double = 1.0

Problem: Unsafe, low-level

  • Here’s a formulation of the eval method using type tests and casts

This is ugly and potentially unsafe.


// Non-solution

def eval(e: Expr): Int = {

if e.isInstanceOf[Number] then

e.asInstanceOf[Number].numValue

else if e.isInstanceOf[Sum] then

eval(e.asInstanceOf[Sum].leftOp)

+ eval(e.asInstanceOf[Sum].rightOp)

else throw Error(s"Unknown expression $e")

}

Non-Solution 3: Object-Oriented Decomposition

Example

Evaluate expressions:


trait Expr {

def eval: Int

}

class Number(n: Int) extends Expr {

def eval: Int = n

}

class Sum(e1: Expr, e2: Expr) extends Expr {

def eval: Int = e1.eval + e2.eval

}

Limitation of OO decomposition

OO decomposition only works well if operations are on a single object.

Example: To simplify expressions, say using the rule:


a * b + a * c -> a * (b + c)

i.e. This is a non-local simplification. It cannot be encapsulated in the method of a single object.

Problem:

If we want a new method, we must define new methods in all the subclasses.

  • OO decomposition mixes data with operations on the data.

  • This can be the right thing if there’s a need for encapsulation and data abstraction.

  • On the other hand, it increases complexity and adds new dependencies to classes.

  • It makes it easy to add new kinds of data but hard to add new kinds of operations.

Also, it causes coupling between data and operations, need to touch all classes to add a new method.

Summary

Problem to solve:

Find a general and convenient way to access heterogeneous data in a class hierarchy.

| Attempts | Issue |

|————————————|———————————————————————————————-|

| Classification and access methods | Quadratic explosion |

| Type tests and casts | Unsafe, low-level |

| Object-oriented decomposition | Causes coupling between data and operations. Need to touch all classes to add a new method. |

Intro to Pattern Matching

Pattern matching is a mechanism for checking a value against a pattern.

A successful match can also deconstruct a value into its constituent parts.

i.e. Pattern matching is a generalization of switching from C/Java to class

hierarchies

Problem Solving

Observation: the sole purpose of test and accessor functions is to reverse

the construction process:

  • Which subclass was used?

  • What were the arguments of the constructor?

This situation is so common that many functional languages, Scala included, automate it.

Functional Decomposition with Pattern Matching

A case class definition is similar to a normal class definition, except it is preceded by the modifier case. For example:


trait Expr

case class Number(n: Int) extends Expr

case class Sum(e1: Expr, e2: Expr) extends Expr

Like before, this defines a trait Expr, and two concrete subclasses Number and Sum.

A match expression has a value, the match keyword, and at least one case clause.


def eval(e: Expr): Int = e match

case Number(n) => n

case Sum(e1, e2) => eval(e1) + eval(e2)

Rules

  • The matching is preceded by a selector expression and is followed by a

sequence of cases, pat => expr.

  • Each case associates an expression expr with a pattern pat.

  • A MatchError exception is thrown if no pattern matches the value of

the selector.

Evaluating Match Expressions

An expression of the form


e match { case p1 => e1 ... case pn => en }
  • Matches the value of the selector e with the patterns p1, ..., pn in the order in which they are written.
  • The match expression is rewritten to the right-hand side of the first case where the pattern matches the selector
  • References to pattern variables are replaced by the corresponding parts in the selector.

What Do Patterns Match?

  • A constructor pattern C(p1, ..., pn) matches all the values of type C (or a subtype) that have been constructed with arguments matching the

patterns p1, ..., pn.

  • A variable pattern x matches any value, and binds the variable’s name to this value.

  • A constant pattern c matches values that are equal to c (in the sense

of ==)

Pattern Matching vs Java Switch

Pattern Matching is a more powerful version of the switch statement in Java. It can likewise be used in place of a series of if/else statements.

Java’s switch is a fall-through statement, which means it executes all the statements after the very first match until it confronts a break statement. In Scala, there’s no break statement. Also, there’s no default case in Scala’s pattern matching. Instead, a wildcard "­_" is used that matches against any other case that has not been covered in previous case statements.

image source: https://subscription.packtpub.com/book/application_development/9781788392822/3/ch03lvl1sec28/pattern-matching

Pattern Matching with Examples

Matching on case classes

Case classes are especially useful for pattern matching.


abstract class Notification

case class Email(sender: String, title: String, body: String) extends Notification

case class SMS(caller: String, message: String) extends Notification

case class VoiceRecording(contactName: String, link: String) extends Notification

Notification is an abstract super class that has three concrete Notification types implemented with case classes Email, SMS, and VoiceRecording. Now we can do pattern matching on these case classes:


def showNotification(notification: Notification): String = {

notification match {

case Email(sender, title, _) =>

s"You got an email from $sender with title: $title"

case SMS(number, message) =>

s"You got an SMS from $number! Message: $message"

case VoiceRecording(name, link) =>

s"You received a Voice Recording from $name! Click the link to hear it: $link"

}

}

val someSms = SMS("12345", "Are you there?")

val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123")

showNotification(someSms) // prints You got an SMS from 12345! // Message: Are you there?

showNotification(someVoiceRecording) // prints You received a Voice Recording from Tom! Click the link to hear it: voicerecording.org/id/123

The function showNotification takes as a parameter the abstract type Notification and matches the type of Notification (i.e. it figures out whether it’s an Email, SMS, or VoiceRecording). In the case Email(sender, title, _), the fields sender and title are used in the return value, but the body field is ignored with _.

Pattern Guards

Pattern guards are simply boolean expressions used to make cases more specific. Just add if <boolean expression> after the pattern.


import week4.*

import week4.Notification.showNotification

def showImportantNotification(notification: Notification, importantPeopleInfo: Seq[String]): String = {

notification match {

case Email(sender, _, _) if importantPeopleInfo.contains(sender) =>

"You got an email from special someone!"

case SMS(number, _) if importantPeopleInfo.contains(number) =>

"You got an SMS from special someone!"

case other =>

showNotification(other) // nothing special, delegate to our original showNotification function

}

}

val importantPeopleInfo = Seq("867-5309", "[email protected]")

val someSms = SMS("123-4567", "Are you there?")

val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123")

val importantEmail = Email("[email protected]", "Drinks tonight?", "I'm free after 5!")

val importantSms = SMS("867-5309", "I'm here! Where are you?")

showImportantNotification(someSms, importantPeopleInfo) // prints You got an SMS from 123-4567! Message: Are you there?

showImportantNotification(someVoiceRecording, importantPeopleInfo) // prints You received a Voice Recording from Tom! Click the link to hear it: voicerecording.org/id/123

showImportantNotification(importantEmail, importantPeopleInfo) // prints You got an email from special someone!

showImportantNotification(importantSms, importantPeopleInfo) // prints You got an SMS from special someone!

In the case Email(sender, _, _) if importantPeopleInfo.contains(sender), the pattern is matched only if the sender is in the list of important people.

Matching Types

Can match on the type like so:


abstract class Device

case class Phone(model: String) extends Device {

def screenOff = "Turning screen off"

}

case class Computer(model: String) extends Device {

def screenSaverOn = "Turning screen saver on..."

}

def goIdle(device: Device) = device match {

case p: Phone => p.screenOff

case c: Computer => c.screenSaverOn

}

goIdle(Computer("Alienware"))

// val res0: String = Turning screen saver on...

def goIdle has different behaviour depending on the type of Device. This is useful when the case needs to call a method on the pattern. It is a convention to use the first letter of the type as the case identifier (p and c in this case).

Match Sealed classes

Traits and classes can be marked sealed, which means all subtypes must be declared in the same file. This assures that all subtypes are known.


sealed abstract class Furniture

case class Couch() extends Furniture

case class Chair() extends Furniture

def findPlaceToSit(piece: Furniture): String = piece match {

case a: Couch => "Lie on the couch"

case b: Chair => "Sit on the chair"

}

findPlaceToSit(Chair())

// val res0: String = Sit on the chair

Summary of Forms of Patterns

Patterns are constructed from:

  • constructors, e.g. Number, Sum,

  • variables, e.g. n, e1, e2,

  • wildcard patterns _,

  • constants, e.g. 1, true.

  • type tests, e.g. n: Number

Variables always begin with a lowercase letter.

The same variable name can only appear once in a pattern. So, Sum(x, x) is not a legal pattern.

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

Cannot Match …

Scala cannot match constructors of regular classes.


class A(a: Int)

A(3) match

// A cannot be used as an extractor in a pattern

// because it lacks an unapply or unapplySeq method

//-- Error: case A(a) => a

case _ => -1

Using the constructor of a regular class in a pattern position results in a compilation error:


A cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method.

Leave a Reply

Your email address will not be published. Required fields are marked *