This is an excerpt from the Scala Cookbook (partially modified for the internet). This is Recipe 19.8, “Examples of how to use types in your Scala classes.”
To put what you’ve learned in this chapter to use, let’s create two examples. First, you’ll create a “timer” that looks like a control structure and works like the Unix time com‐ mand. Second, you’ll create another control structure that works like the Scala 2.10.x Try/ Success/Failure classes.
Example 1: Creating a 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 command returns the results of the find
command it was given, along with the time it took to run. This can be a helpful way to troubleshoot performance problems. You can create a similar timer
method in Scala to let you run code like this:
val (result, time) = timer(someLongRunningAlgorithm) println(s"result: $result, time: $time")
In this example, the timer
runs a method named someLongRunningAlgorithm
, and then returns the result from the algorithm, along with the algorithm’s execution time. You can see how this works by running a simple 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 is surprisingly simple, and involves the use of a generic type parameter:
def timer[A](blockOfCode: => A) = { val startTime = System.nanoTime val result = blockOfCode val stopTime = System.nanoTime val delta = stopTime - startTime (result, delta/1000000d) }
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 to be a generic type parameter. This lets you pass all sorts of algorithms into timer
, including those that return nothing:
scala> val (result, time) = timer{ println("Hello") } Hello result: Unit = () time: Double = 0.544
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 is a simple use of specifying 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 code to look like this with Scala 3:
/** * 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
For a few moments go back in time, and 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 for now.) 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 class named Attempt
, and because you know you don’t want to use the new
keyword to create a new instance, you know that you need a companion object with an apply
method. You further realize that you need to define Succeeded
and Failed
, and they should extend Attempt
.
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
.
Thinking about the API you want, you know the getOrElse
method should 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 2.10 Try classes
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 |