Saturday, July 9, 2011

Prefer recursion to var's and while loops

A while back, when I was first picking up Scala, I needed to prompt the user for a line of text and keep prompting until I got a non-blank line.

Apparently, I threw the following together:

def readCompleteLine(prompt: String) = {
  val reader = new jline.ConsoleReader()
  reader.setDefaultPrompt(prompt)

  var line: String = null

  do {
    line = reader.readLine()
  } while( line != null && line.length == 0 )

  line
}

I came across this function yesterday and involuntarily made a gross-out sound. var's, mutability, a while loop (a do-while loop, no less!) and even the dread null. There had to be a better way.

Using higher-order functions and recursion, I rewrote this to be less imperative, more readable and more general:

def readUntil[X](prompt: String, transform: String => X, pred: X => Boolean): X =  {
  val reader = new jline.ConsoleReader() { setDefaultPrompt(prompt)}

  @tailrec def ruHelper: X = Option(reader.readLine()).map(transform(_)) match {
    case Some(x) if pred(x) => x
    case _ => ruHelper
  }

  ruHelper
}

/* simple, string-only version */
def readUntil(prompt: String, pred: String => Boolean): String =  {
  readUntil(prompt, x => x, pred)
}

Using this as a primitive, it's easy to write a method that repeatedly prompts the user for a string until it's non-empty.

def readUntilNonBlankLine(prompt: String) = readUntil(prompt, _.length > 0)

But it's also easy to create a method that only allows input from a list of accepted strings:

def readOneOf(prompt: String, acceptableValues: List[String]): String = 
    readUntil(prompt, acceptableValues.contains(_))

Or even a function that reads until the value is a properly-formed floating-point number:

import util.control.Exception._

def tryToReadDouble(s: String) = 
  catching(classOf[NumberFormatException]) opt (s.toDouble)

def readUntilValidDouble(prompt: String) = ReadingUtils.readUntil(prompt,
      tryToReadDouble(_), (s: Option[Double]) => s.isDefined).get

Higher-order functions and genericity can really pay off - the base readUntil
is now a nicely abstracted primitive that I can test in isolation and then plug in elsewhere in my program, without having to write and rewrite error-prone imperative code.

No comments:

Post a Comment