(This article is an excerpt from the 1st Edition of the Scala Cookbook.)
To put what you’ve learned in this chapter to use, let’s create two examples:
- First, you’ll create a
timer
method that looks like a Scala control structure, and works like the Unixtime
command. - Second, you’ll create another control structure that works like the
Try
/Success
/Failure
classes that were included with Scala 2.10.
Example 1: Creating a Scala Timer
On Unix systems you can run a time
command (timex
on some systems) to see how long commands take to execute:
$ time find . -name "*.scala"
That time
command returns the results of the find
command it was given as input, along with the time it took to run. This can be a helpful way to troubleshoot performance problems.
You can create similar functionality in Scala to let you run a timer like this:
val (result, time) = timer(someLongRunningAlgorithm) println(s"result: $result, time: $time")
In this example, the timer
runs a method named longRunningAlgorithm
, and then returns (a) the result from the algorithm, along with (b) the algorithm’s execution time.
Notice that
timer
returns two values as a two-element tuple, and I have named those two valuesresult
andtime
.
You can also pass a block of code into the timer
method like this:
val (result, time) = timer { your block of code here ... }
Assuming for a moment that the timer
method already exists, you can see how this works by running an example in the REPL:
scala> val (result, time) = timer { Thread.sleep(500); 1 } result: Int = 1 time: Double = 500.32
As expected, the code block returns the value 1
, with an execution time of about 500 ms.
The timer code
As computer programming goes, the timer
code is relatively simple, and involves the use of a generic type parameter:
def timer[A](blockOfCode: => A) = { val startTime = System.nanoTime val result = blockOfCode // the "block of code" you pass in is run here val stopTime = System.nanoTime val delta = stopTime - startTime (result, delta/1000000d) // return the result and time as a Tuple }
The timer
method uses Scala’s call-by-name syntax to accept a block of code as a parameter. Rather than declare a specific return type from the method (such as Int
), you declare the return type A
to be a generic type parameter. This lets you pass all sorts of algorithms into timer, including those that return nothing (or more accurately, the Unit
type):
scala> val (result, time) = timer { println("Hello") } Hello result: Unit = () time: Double = 0.144
Or an algorithm that reads a file and returns an iterator:
scala> def readFile(filename: String) = io.Source.fromFile(filename).getLines readFile: (filename: String)Iterator[String] scala> val (result, time) = timer{ readFile("/etc/passwd") } result: Iterator[String] = non-empty iterator time: Double = 32.119
This example shows how to specify a generic type in a non-collection class, and helps you get ready for the next example.
The updated timer code
As a brief note, I recently updated the timer
function to work with Scala 3, and it look like this:
/** * Note that `timer { Thread.sleep(2_000) }` is * returned as `2007.09575` or something close * to that. */ def timer[A](blockOfCode: => A): (A, Double) = val startTime = System.nanoTime val result = blockOfCode val stopTime = System.nanoTime val delta = stopTime - startTime (result, delta/1_000_000d)
Example 2: Writing Your Own “Try” Classes
Imagine the days back before Scala 2.10 when there was no such thing as the Try
, Success
, and Failure
classes in scala.util. (They were available from Twitter, but just ignore that, too.) In those days you might have come up with your own solution that you called Attempt
, Succeeded
, and Failed
that would let you write code like this:
val x = Attempt("10".toInt) // Succeeded(10) val y = Attempt("10A".toInt) // Failed(Exception)
To enable this basic API, you realize you’ll need a few things:
- A class named
Attempt
- Because of its syntax, you know right away that you need a companion object with an
apply
method - You further realize that you need to define
Succeeded
andFailed
, and they should extendAttempt
Therefore, you begin with this code, placed in a file named Attempt.scala:
// version 1 sealed class Attempt[A] object Attempt { def apply[A](f: => A): Attempt[A] = try { val result = f return Succeeded(result) } catch { case e: Exception => Failed(e) } } final case class Failed[A](val exception: Throwable) extends Attempt[A] final case class Succeeded[A](value: A) extends Attempt[A]
In a manner similar to the previous timer
code, the apply
method takes a call-by-name parameter, and the return type is specified as a generic type parameter. In this case the type parameter ends up sprinkled around in other areas. Because apply
returns a type of Attempt
, it’s necessary there; because Failed
and Succeeded
extend Attempt
, it’s propagated there as well.
This first version of the code lets you write the basic x
and y
examples. However, to be really useful, your API needs a new method named getOrElse
that lets you get the information from the result, whether that result happens to be a type of Succeeded
or Failed
.
Designing the API first, you realize you want the getOrElse
method to be called like this:
val x = Attempt(1/0) val result = x.getOrElse(0)
Or this:
val y = Attempt("foo".toInt).getOrElse(0)
To enable a getOrElse
method, make the following changes to the code:
// version 2 sealed abstract class Attempt[A] { def getOrElse[B >: A](default: => B): B = if (isSuccess) get else default var isSuccess = false def get: A } object Attempt { def apply[A](f: => A): Attempt[A] = try { val result = f Succeeded(result) } catch { case e: Exception => Failed(e) } } final case class Failed[A](val exception: Throwable) extends Attempt[A] { isSuccess = false def get: A = throw exception } final case class Succeeded[A](result: A) extends Attempt[A] { isSuccess = true def get = result }
The variable isSuccess
is added to Attempt
so it can be set in Succeeded
or Failed
. An abstract method named get
is also declared in Attempt
so it can be implemented in the two subclasses. These changes let the getOrElse
method in Attempt
work.
The getOrElse
method signature is the most interesting thing about this new code:
def getOrElse[B >: A](default: => B): B = if (isSuccess) get else default
Because of the way getOrElse
works, it can either return the type A
, which is the result of the expression, or type B
, which the user supplies, and is presumably a substitute for A
. The expression B >: A
is a lower bound. Though it isn’t commonly used, a lower bound declares that a type is a supertype of another type. In this code, the term B >: A
expresses that the type parameter B
is a supertype of A
.
The Scala Try/Success/Failure types
You could keep developing your own classes, but the Try
, Success
, and Failure
classes in the scala.util package were introduced in Scala 2.10, so this is a good place to stop.
However, it’s worth noting that these classes can be a great way to learn about Scala types. For instance, the getOrElse
method in the Attempt
code is the same as the getOrElse
method declared in Try
:
def getOrElse[U >: T](default: => U): U = if (isSuccess) get else default
The map
method declared in Success
shows how to define a call-by-name parameter that transforms a type T
to a type U
:
def map[U](f: T => U): Try[U] = Try[U](f(value))
Its flatten
method uses the <:<
symbol that wasn’t covered in this chapter. When used as A <:< B
, it declares that “A must be a subtype of B.” Here’s how it’s used in the Success
class:
def flatten[U](implicit ev: T <:< Try[U]): Try[U] = value
When it comes to learning about generic parameter types, these classes are interesting to study. They’re self-contained, and surprisingly short. The Scala collections classes also demonstrate many more uses of generics.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |