Scala: How to build control structures with types and call-by-name parameters

(This article is an excerpt from 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 Unix time command. Second, you’ll create another control structure that works like the Scala 2.10 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 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 the result from the algorithm, along with the algorithm’s execution time.

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

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          // 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:

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 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

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 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.

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.

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.

... this post is sponsored by my books ...
 

Back to top