CompSci220 - GitHub Pagesumass-cs-220.github.io/weeks/09/15-fp-error-handling.pdf · Objectives •...
Transcript of CompSci220 - GitHub Pagesumass-cs-220.github.io/weeks/09/15-fp-error-handling.pdf · Objectives •...
CompSci 220Programming Methodology
15: Understanding FP Error Handling Part 1
Objectives
• Understanding Exceptions• Understanding RT with Exceptions• Handling Errors without Exceptions• The Option data type
What is an exceptional situation?
• What is an exceptional situation/condition in a program?
What is an exceptional situation?
• What is an exceptional situation/condition in a program?
• A situation when something occurs in a program that was unexpected or erroneous.
What is an exceptional situation?
• What is an exceptional situation/condition in a program?
• A situation when something occurs in a program that was unexpected or erroneous. For example:
What is an exceptional situation?
• What is an exceptional situation/condition in a program?
• A situation when something occurs in a program that was unexpected or erroneous. For example:• A file does not exist when an attempt is made to open it.
What is an exceptional situation?
• What is an exceptional situation/condition in a program?
• A situation when something occurs in a program that was unexpected or erroneous. For example:• A file does not exist when an attempt is made to open it.• The network does not exist when an attempt is made to communicate.
What is an exceptional situation?
• What is an exceptional situation/condition in a program?
• A situation when something occurs in a program that was unexpected or erroneous. For example:• A file does not exist when an attempt is made to open it.• The network does not exist when an attempt is made to communicate.• An attempt to reference an object that is null.
What is an exceptional situation?
• What is an exceptional situation/condition in a program?
• A situation when something occurs in a program that was unexpected or erroneous. For example:• A file does not exist when an attempt is made to open it.• The network does not exist when an attempt is made to communicate.• An attempt to reference an object that is null.• An attempt to execute an illegal instruction.
What is an exceptional situation?
• What is an exceptional situation/condition in a program?
• A situation when something occurs in a program that was unexpected or erroneous. For example:• A file does not exist when an attempt is made to open it.• The network does not exist when an attempt is made to communicate.• An attempt to reference an object that is null.• An attempt to execute an illegal instruction.• An invalid argument is provided to a function.
What is an exceptional situation?
• What is an exceptional situation/condition in a program?
• A situation when something occurs in a program that was unexpected or erroneous. For example:• A file does not exist when an attempt is made to open it.• The network does not exist when an attempt is made to communicate.• An attempt to reference an object that is null.• An attempt to execute an illegal instruction.• An invalid argument is provided to a function.• The run-‐time stack is exhausted (stack overflow exception).
What is a non exceptional situation?
• Any situation in a program that is expected or possible.
What is a non exceptional situation?
• Any situation in a program that is expected or possible.For example:
What is a non exceptional situation?
• Any situation in a program that is expected or possible.For example:• Retrieving a value from a Map that might not exist.
What is a non exceptional situation?
• Any situation in a program that is expected or possible.For example:• Retrieving a value from a Map that might not exist.• Reading a program with syntax errors.
What is a non exceptional situation?
• Any situation in a program that is expected or possible.For example:• Retrieving a value from a Map that might not exist.• Reading a program with syntax errors.• Compiling a program with type errors.
What is a non exceptional situation?
• Any situation in a program that is expected or possible.For example:• Retrieving a value from a Map that might not exist.• Reading a program with syntax errors.• Compiling a program with type errors.• Reading bytes from a network that were corrupted.
What is a non exceptional situation?
• Any situation in a program that is expected or possible.For example:• Retrieving a value from a Map that might not exist.• Reading a program with syntax errors.• Compiling a program with type errors.• Reading bytes from a network that were corrupted.• Reading bytes from a file that was corrupted.
What is a non exceptional situation?
• Any situation in a program that is expected or possible.For example:• Retrieving a value from a Map that might not exist.• Reading a program with syntax errors.• Compiling a program with type errors.• Reading bytes from a network that were corrupted.• Reading bytes from a file that was corrupted.
• Typically, a non exceptional situation is one in which regular control flow and possible conditions can be determined and reported.
Traditional Error Handling
• How did earlier languages (e.g., C) handle exceptional situations?• Integer return values would indicate success/failure.
// returns a non-negative integer (if successful) (file descriptor)int open(char* filename);
// returns number of bytes read, 0 if at end of file, -1 if errorint read(FILE* f, char** buf);
Traditional Error Handling
• How did earlier languages (e.g., C) handle exceptional situations?• Integer return values would indicate success/failure.
// returns a non-negative integer (if successful) (file descriptor)int open(char* filename);
// returns number of bytes read, 0 if at end of file, -1 if errorint read(FILE* f, char** buf);
• What if it is difficult to determine what that return value should be.
Consider what error value you would use to access a map of ints?
Error Handling with Exceptions
• Because C and other languages typically do not support multiple return values the idea of throwing exceptions came to fruition.
• When an exceptional condition occurs, “throw” an exception that will be “caught” by a surrounding try/catch block.
• If the try/catch block is not in the current function scope, prematurely escape from the function (not return) to find a try/catch block. Continue to do this until one is found or crash the program with a stack trace.
Why are exceptions a problem in FP?
a) They are not a problem.b) They can only check non exceptional situations.c) They break referential transparency.d) They are not supported in functional languages.e) All of these.
Referential Transparency
• Why do exceptions break referential transparency?
def failingFn(i: Int): Int =val y: Int = throw new Exception(“fail!”)try {
val x = 42 + 5x + y
}catch { case e: Exception => 43 }
A catch block is just a pattern matching block like the ones we’ve seen.
case e: Exception is a pattern that matches any Exception.
Referential Transparency
• Why do exceptions break referential transparency?
def failingFn(i: Int): Int =val y: Int = throw new Exception(“fail!”)try {
val x = 42 + 5x + y
}catch { case e: Exception => 43 }
scala> failingFn(12)java.lang.Exception: fail!at .failingFn(<console>:8)……
Referential Transparency
• Why do exceptions break referential transparency?
def failingFn(i: Int): Int =val y: Int = throw new Exception(“fail!”)try {
val x = 42 + 5x + y
}catch { case e: Exception => 43 }
scala> failingFn(12)java.lang.Exception: fail!at .failingFn(<console>:8)……
We can prove that y is referentiallytransparent. Do you remember how?
Referential Transparency
• Why do exceptions break referential transparency?
def failingFn(i: Int): Int =val y: Int = throw new Exception(“fail!”)try {
val x = 42 + 5x + y
}catch { case e: Exception => 43 }
scala> failingFn(12)java.lang.Exception: fail!at .failingFn(<console>:8)……
We can prove that y is referentiallytransparent. Do you remember how?
Right, by forward substituting y with the value it refers to – this substitution should preserve the meaning of the program.
Referential Transparency
• Why do exceptions break referential transparency?
def failingFn(i: Int): Int =try {
val x = 42 + 5x + ((throw new Exception(“fail!”)): Int)
}catch { case e: Exception => 43 }
Here, we have forward substituted the value of y into the x + y expression.
What happens if we run this?
Referential Transparency
• Why do exceptions break referential transparency?
def failingFn(i: Int): Int =try {
val x = 42 + 5x + ((throw new Exception(“fail!”)): Int)
}catch { case e: Exception => 43 }
Clearly, the meaning is not preserved so y is not RT and neither is failingFn.
scala> failingFn(12)res1: Int = 43
Contextual Requirements
• The meaning of an RT expression does not depend on context.• It can be reasoned about locally
Contextual Requirements
• The meaning of an RT expression does not depend on context.• It can be reasoned about locally
• The meaning of non-‐RT expressions is context-‐dependent.• It requires global reasoning
Contextual Requirements
• The meaning of an RT expression does not depend on context.• It can be reasoned about locally
• The meaning of non-‐RT expressions is context-‐dependent.• It requires global reasoning
• For example, the meaning of 42 + 5 doesn’t depend on the larger expression it is embedded in – it will always be 47.
Contextual Requirements
• The meaning of an RT expression does not depend on context.• It can be reasoned about locally
• The meaning of non-‐RT expressions is context-‐dependent.• It requires global reasoning
• For example, the meaning of 42 + 5 doesn’t depend on the larger expression it is embedded in – it will always be 47.• But, the meaning of throw new Exception(“fail!”) is context-‐dependent. It takes on a different meaning depending on which tryblock it is nested within.
Exceptions: Problem 1
• Exceptions break RT and introduce context dependence.
This moves us away from simple reasoning of the substitution model and making it possible to write confusing exception-‐based code.
This is the source of the folklore advice that exceptions should be used only for error handling (unexpected situations), not for control flow.
Exceptions: Problem 2
• Exceptions are not type-‐safe.
The type of failingFn, Int => Int, tells us nothing about the fact that exceptions may occur.
The compiler will certainly not force callers of failingFn to make decisions about how to handle those exceptions. If we forget, they will not be detected until runtime.
Java forces you to make a decision for “checked” exceptions. But, this results in substantial boilerplate code and does not work for higher-‐order functions. (i.e., how would you implement List.map?)
Why are exceptions a problem in FP?
a) They are not a problem.b) They can only check non exceptional situations.c) They break referential transparency.d) They are not supported in functional languages.e) All of these.
Solution: Old Idea / New Context
• We’d like an alternative to exceptions without these drawbacks.
• But, we do not want to lose out on the primary benefit of exceptions: they allow us to consolidate and centralize error-‐handling logic.
Solution: Old Idea / New Context
• We’d like an alternative to exceptions without these drawbacks.
• But, we do not want to lose out on the primary benefit of exceptions: they allow us to consolidate and centralize error-‐handling logic.
• The functional approach goes back to the C approach:instead of throwing an exception, we return a value indicating that an exceptional condition has occurred.
• Unlike C – this approach will be type-‐safe.
Alternative to Exceptions
• Consider a function that computes the mean of a list, which is undefined if the list is empty:
Alternative to Exceptions
• Consider a function that computes the mean of a list, which is undefined if the list is empty:
def mean(xs: Seq[Double]): Double =if (xs.isEmpty)
throw new ArithmeticException(“mean of empty list!”)else xs.sum / xs.length
Alternative to Exceptions
• Consider a function that computes the mean of a list, which is undefined if the list is empty:
def mean(xs: Seq[Double]): Double =if (xs.isEmpty)
throw new ArithmeticException(“mean of empty list!”)else xs.sum / xs.length
The mean function is an example of a partial function: it’s not defined for some inputs (and not implied by the input types).
What is a possible alternative?
Alternative to Exceptions
• Consider a function that computes the mean of a list, which is undefined if the list is empty:
def mean(xs: Seq[Double]): Double =if (xs.isEmpty)
0.0/0.0else xs.sum / xs.length
We could return the result of 0.0/0.0 which would be Double.NaN.
Alternative to Exceptions
• Consider a function that computes the mean of a list, which is undefined if the list is empty:
def mean(xs: Seq[Double]): Double =if (xs.isEmpty)
0.0/0.0else xs.sum / xs.length
We could return the result of 0.0/0.0 which would be Double.NaN. But, …
Alternative to Exceptions
• Consider a function that computes the mean of a list, which is undefined if the list is empty:
def mean(xs: Seq[Double]): Double =if (xs.isEmpty)
0.0/0.0else xs.sum / xs.length
1) It allows errors to silently propagate (caller forgets to check)
Alternative to Exceptions
• Consider a function that computes the mean of a list, which is undefined if the list is empty:
def mean(xs: Seq[Double]): Double =if (xs.isEmpty)
0.0/0.0else xs.sum / xs.length
1) It allows errors to silently propagate (caller forgets to check)2) Boilerplate code to check for “real” results
Alternative to Exceptions
• Consider a function that computes the mean of a list, which is undefined if the list is empty:
def mean(xs: Seq[Double]): Double =if (xs.isEmpty)
0.0/0.0else xs.sum / xs.length
1) It allows errors to silently propagate (caller forgets to check)2) Boilerplate code to check for “real” results3) It’s not applicable to polymorphic code (what would the return value be?)
Alternative to Exceptions
• Consider a function that computes the mean of a list, which is undefined if the list is empty:
def mean(xs: Seq[Double]): Double =if (xs.isEmpty)
0.0/0.0else xs.sum / xs.length
1) It allows errors to silently propagate (caller forgets to check)2) Boilerplate code to check for “real” results3) It’s not applicable to polymorphic code (what would the return value be?)4) Demands a special policy on how you use mean that has nothing to do with mean.
Alternative to Exceptions
• We could also redefine mean to supply an argument that tells us what to do in case we don’t know how to handle the input:
def mean(xs: Seq[Double], onEmpty: Double): Double =if (xs.isEmpty) onEmptyelse xs.sum / xs.length
Now, mean is a total function – but it requires the caller to know how to handle the undefined case and limits them to returning a Double.
Still, how do we know if it was undefined or our default value?
The Option Data Type
• The solution is to represent explicitly in the return type that a function may not always have an answer.
• The Option data type allows us to defer to the caller for the error-‐handling strategy.
sealed trait Option[+A]case class Some[+A](get: A) extends Option[A]case object None extends Option[Nothing]
Mean with Option
def mean(xs: Seq[Double]): Option[Double] =if (xs.isEmpty) Noneelse Some(xs.sum / xs.length)
The return type now reflects the possibility that the result may not always be defined.
We still always return a result of the declared type (Option[Double]) from our function – so mean is now a total function.
Usage Patterns for Option
• There are many places where Option is used.• It is quite useful to use in your own programs to replace exceptions.• It appears in the Scala Standard Library:
• Map lookup for a given key returns Option
• List defines headOption and lastOption, both returning option containg the first or last elements if non-‐empty.
Functions on Option
trait Option[+A] {def map[B](f: A => B): Option[B]def flatMap[B](f: A => Option[B]): Option[B]def getOrElse[B >: A](default: => B): Bdef orElse[B >: A](ob: => Option[B]): Option[B]def filter(f: A => Boolean): Option[A]
}
The Option type has several useful functions that allow us to transparently operate on an Option value.
Functions on Option
trait Option[+A] {def map[B](f: A => B): Option[B]def flatMap[B](f: A => Option[B]): Option[B]def getOrElse[B >: A](default: => B): Bdef orElse[B >: A](ob: => Option[B]): Option[B]def filter(f: A => Boolean): Option[A]
}
Apply f if the Option is not None – it transparently unwraps/wraps a Some(x) and returns either None or Some(f(x)).
Functions on Option
trait Option[+A] {def map[B](f: A => B): Option[B]def flatMap[B](f: A => Option[B]): Option[B]def getOrElse[B >: A](default: => B): Bdef orElse[B >: A](ob: => Option[B]): Option[B]def filter(f: A => Boolean): Option[A]
}
Apply f, which may fail, to the Option if not None:Some(4).flatMap((v) => Some(v+1)) == Some(5)
Functions on Option
trait Option[+A] {def map[B](f: A => B): Option[B]def flatMap[B](f: A => Option[B]): Option[B]def getOrElse[B >: A](default: => B): Bdef orElse[B >: A](ob: => Option[B]): Option[B]def filter(f: A => Boolean): Option[A]
}
Returns the value of the Option or the default if the Option is None.Some(5).getOrElse(6) == 5, None.getOrElse(6) == 6
Functions on Option
trait Option[+A] {def map[B](f: A => B): Option[B]def flatMap[B](f: A => Option[B]): Option[B]def getOrElse[B >: A](default: => B): Bdef orElse[B >: A](ob: => Option[B]): Option[B]def filter(f: A => Boolean): Option[A]
}
Don’t evaluate ob unless needed.Some(5).orElse(Some(6)) == Some(5), None.orElse(Some(6)) == Some(6)
Functions on Option
trait Option[+A] {def map[B](f: A => B): Option[B]def flatMap[B](f: A => Option[B]): Option[B]def getOrElse[B >: A](default: => B): Bdef orElse[B >: A](ob: => Option[B]): Option[B]def filter(f: A => Boolean): Option[A]
}
Convert Some to None if the value doesn’t satisfy f.Some(4).filter(_ > 5) == None, Some(4).filter(_ < 5) == Some(4)
Option Usage Scenarios
case class Employee(name: String, dept: String)
def lookupByName(name: String): Option[Employee] = …
val joeDepartment: Option[String] =lookupByName(“Joe”).map(_.dept)
Here, we get Joe’s department if Joe is an employee or None if Joe is not an employee.
Option Usage Scenarios
case class Employee(name: String, dept: String)
def lookupByName(name: String): Option[Employee] = …
val joeDepartment: String =lookupByName(“Joe”).map(_.dept)
How might we extend this to return the department as a String, or “Not an employee” if “Joe” is not an employee?Write this down on paper! def map[B](f: A => B): Option[B]
def flatMap[B](f: A => Option[B]): Option[B]def getOrElse[B >: A](default: => B): Bdef orElse[B >: A](ob: => Option[B]): Option[B]def filter(f: A => Boolean): Option[A]
Option Usage Scenarios
case class Employee(name: String, dept: String)
def lookupByName(name: String): Option[Employee] = …
val joeDepartment: String =lookupByName(“Joe”).map(_.dept).getOrElse(“Not an employee”)
How might we extend this to return the department as a String, or “Not an employee” if “Joe” is not an employee?Write this down on paper! def map[B](f: A => B): Option[B]
def flatMap[B](f: A => Option[B]): Option[B]def getOrElse[B >: A](default: => B): Bdef orElse[B >: A](ob: => Option[B]): Option[B]def filter(f: A => Boolean): Option[A]
Option Usage Scenarios
case class Employee(name: String, dept: String)
def lookupByName(name: String): Option[Employee] = …
val joeDepartment: String =lookupByName(“Joe”).???
Assuming Joe is an employee, how might we return the department name if Joe is not in the “Accounting” department and“Default Dept.” if he is in “Accounting”?Write this down on paper!
def map[B](f: A => B): Option[B]def flatMap[B](f: A => Option[B]): Option[B]def getOrElse[B >: A](default: => B): Bdef orElse[B >: A](ob: => Option[B]): Option[B]def filter(f: A => Boolean): Option[A]
Option Usage Scenarios
case class Employee(name: String, dept: String)
def lookupByName(name: String): Option[Employee] = …
val joeDepartment: String =lookupByName(“Joe”).map(_.dept).filter(_ != “Accounting”).getOrElse(“Default Dept.”)
Assuming Joe is an employee, how might we return the department name if Joe is not in the “Accounting” department and“Default Dept.” if he is in “Accounting”?Write this down on paper!
def map[B](f: A => B): Option[B]def flatMap[B](f: A => Option[B]): Option[B]def getOrElse[B >: A](default: => B): Bdef orElse[B >: A](ob: => Option[B]): Option[B]def filter(f: A => Boolean): Option[A]