A function should validate its inputs before anything else. But what to do if the inputs aren't valid? It's seldom the case that the place errors are detected are also the place they can be handled. For example, a CAD system might have use for a sqrt function in some back-end math library. If this function is called with a negative input, we don't want the sqrt function to report this directly to the user. This should be handled in the front-end user interface.
Recall from Java that exceptions can be thrown in two ways:
· Explicitly, using throw.
· Implicitly, by calling a function that throws an exception that you don't catch.
Don't catch an exception if you don't know how to handle it.
Here's an example: Account.scala.
Here's a sample session:
Enter amount to withdraw $50
new balance = $50.0
Type "quit" to quit
Enter amount to withdraw $-10
Sorry, amount must be positive
Type "quit" to quit
Enter amount to withdraw $ten
Sorry, amount must be a number
Type "quit" to quit
Enter amount to withdraw $60
Insufficient funds, current balance = $50.0
Type "quit" to quit
Enter amount to withdraw $quit
Type "quit" to quit
bye
The file contains the declaration of three classes:
class NegativeInputException
class InsufficientFundsException
class Account
There will be much more on defining Scala classes later. For now notice that the first two classes extend Java's Exception class. Therefore, they can be thrown and caught. We can think of this as an exception hierarchy tailored to the kinds of errors we anticipate our account holders might make.
The withdraw method checks its input first and throws the appropriate exceptions. We don't need "else" or "new".
Notice that Scala doesn't require programmers to declare the exceptions a function might throw.
Our test harness is a simple REPL (read-execute-print loop). Exceptions are thrown and caught inside the loop. We don't want the loop to terminate because of a simple user input error.
In Java I would use a break statement to break out of the loop when the user enters "quit". For some reason the designers of Scala decided to leave break and continue out. (There is a break library function that was added later.) Like Java, Scala doesn't have a goto statement.
The syntax for the catch block resembles the syntax for a match block. It examines the type of the exception thrown to decide which action to take. Of course the order is important as the first matching type is selected.
Notice that amt.toDouble might throw a number format exception. In this case withdraw implicitly throws the exception.
There are two types of exceptions: user and system. I try to catch and handle all of the user exceptions. But otherwise I assume the exception is system-level, like out-of-memory. In this case I quit out of the loop. There's no use in continuing.
In Java I would use a break statement to break out of the loop when the user enters "quit". For some reason the designers of Scala decided to leave break and continue out. (There is a break library function that was added later.) Like Java, Scala doesn't have a goto statement. Although Scala does have return, it's mostly used as a way to escape a loop or error.
Notice that the type of the withdraw method is a lie. It promises that given any input of type Double it will produce an output of type Double. But this isn't true. If the input is negative or too big.
Functional programmers might be bothered by this lie. For them Scala provides optional values.
An option is a container object that is either empty—None-- or containing a single value of some type T-- Some(value).
In Scala, consider using options instead of returning null:
def milesToKilometers(miles:
Double): Option[Double] =
if (miles
< 0) None else Some(miles * 1.60934)
One nice feature of using options is that we can pull them apart using match expressions:
def kilometersToMeters(km:
Option[Double]): Option[Double] =
km match {
case None => None
case Some(x) => Some(x * 1000)
}
}
kilometersToMeters(milesToKilometers(65)) // = Some(104607.1)
kilometersToMeters(milesToKilometers(-65))
// = None
Some and Option are examples of generic classes. In Scala types always appear in braces.
None is a singleton extending Option[T] and therefore can be used in any context where an option is expected.
Some[T] extends Option[T] and therefore can be used in any context where an option is expected.
We can use options instead of throwing exceptions for handling errors, but this isn't recommended.
Here's another version of Account.scala.
Here's a sample session:
Enter amount to withdraw $50
new balance = $50.0
Enter amount to withdraw $60
Invalid input: 60
Enter amount to withdraw $-10
Invalid input: -10
Enter amount to withdraw $ten
Invalid input: ten
Enter amount to withdraw $quit
bye
In this version we can't distinguish between insufficient funds, negative amounts, or non-numeric inputs. They all return None.
If a function f calls a function g that returns an optional value, then f needs to check if None was returned. Usually f will return None, too. This can mean lots of extra coding. For example:
object timeConversions
def yearsToWeeks(y: Int): Option[Int] =
if (y < 0) None else Some(52 * y)
def yearsToDays(y: Int): Option[Int] =
yearsToWeeks(y) match
case None => None
case Some(w) => Some(7 * w)
def yearsToHours(y: Int): Option[Int] =
yearsToDays(y) match
case None => None
case Some(d) => Some(24 * d)
In some languages (Lambda Calculus) defining and composing functions are the only things programmers can do. Composing two functions is easy if the range of one matches the domain of the other. For example:
def yearsToHours(y: Int): Int = y * 365 * 24
def hoursToMinutes(h: Int): Int = 60 * h
def yearsToMinutes(y: Int): Int =
hoursToMinutes(yearsToHours(y)) //
function composition
Composing functions that return options is a little trickier:
def hoursToMinutes(h: Int): Option[Int] =
if (h < 0) None else Some(60 * h)
def yearsToMinutes(y: Int): Option[Int] =
yearsToHours(y) match
case None => None
case Some(h) => hoursToMinutes(h)