Examples of how to use types in your Scala classes (generics, call-by-name parameters)

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.

Back to top

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.

Back to top

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.

Back to top

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.

Back to top

The Scala Cookbook

This tutorial is sponsored by the Scala Cookbook, which I wrote for O’Reilly:

You can find the Scala Cookbook at these locations:

Back to top

Add new comment

The content of this field is kept private and will not be shown publicly.

Anonymous format

  • Allowed HTML tags: <em> <strong> <cite> <code> <ul type> <ol start type> <li> <pre>
  • Lines and paragraphs break automatically.
By submitting this form, you accept the Mollom privacy policy.