Sunday, September 27, 2009

Scala: Beyond Hello World

I overheard someone at work last week asking another guy how he would go about implementing string-wrapping in Java. Memories came flooding back of university functional programming assignments where we had to use Miranda to shape nursery rhymes into a trapezoid.

I listened to the continuing conversation and realised that the solution being discussed - which involved seeking through the string, remembering where the last whitespace was and cutting off substrings at appropriate points - was obviously imperative in nature. "Not that there's anything wrong with that." If I wasn't reading a book about Scala programming at the moment I would have joined right in. But my functionally-leaning mind instantly started to wonder what the functional solution would be.

It sounded like a simple but interesting problem to try and solve with a new language - a nice "real" program to attempt after Hello World. So I sat down after work and whipped up a small Scala program to wrap words. I have to admit, it took a bit longer than I expected - over an hour. Though a large part of that was spent looking up docs for the List class and trying to solve new and bemusing syntax errors, I do recall getting distracted by an investigation into when tail call optimisation does and doesn't occur.

If you're new to Scala and looking for a little problem to help you learn some syntax and waste an hour, I can recommend word-wrapping as a suitable foe. My solution is below. If you come up with your own, I'd love to know what you did differently!


object Wrapper {

type Line = List[String]

def main(args: Array[String]) {
for (i <- 1 to 31) print(if (i == 31) '\n' else '.')
val s = "This is a really, really long line of text that, hopefully, " +
"will be long enough to sufficiently test a function that must " +
"divide a really, really long string into lines that are no " +
"more than 30 (that's thirty) characters long."
for (line <- wrap(s)) {
for (string <- line) {
print(string + " ")
}
println
}
}

def wrap(stringToWrap : String) : List[Line] =
wrap(List.fromArray(stringToWrap.split(" ")), 30, List(List()))

def wrap(stringsToWrap : List[String],
maxLineLength : Int,
lines : List[Line]) : List[Line] =
stringsToWrap match {
case Nil => lines.reverse
case nextString :: remainingStrings =>
if (lineLength(lines.head) + nextString.length <= maxLineLength)
wrap(remainingStrings, maxLineLength,
(lines.head ::: List(nextString)) :: lines.tail)
else {
wrap(remainingStrings, maxLineLength, List(nextString) :: lines)

}
}

def lineLength(s : Line) : Int = s match {
case Nil => 0
case head :: tail => 1 + head.length + lineLength(tail)
}

}

2 comments:

  1. Hi Graham,
    nice Blog!
    I'm a Scalastarter and - asumably - about as skillful as you were a year ago...
    First thing I noticed when scanning your implementation was this feeling: "there must be a way to express this shorter"
    anyway - I made some improvements:

    1. in Main:
    ...
    println(("."*30))
    val w2 = wrap2(s, 30)
    w2.foreach(println)

    2. now my wrap:
    def wrap2(stringToWrap : String, lineWidth : Int):List[String] = {
    if(stringToWrap.isEmpty){
    Nil
    }else if(stringToWrap.length()<=lineWidth){
    List(stringToWrap)
    }else{
    val parts = stringToWrap.splitAt(lineWidth+1) // parts = lineMax, restOfString
    val blankIndex = parts._1.lastIndexOf(" ") + 1;
    val line = parts._1.splitAt(if(blankIndex>0)blankIndex else lineWidth);
    val restOfString = line._2.concat(parts._2)
    line._1 :: wrap2(restOfString, lineWidth);
    }
    }

    ----
    and still I hope this can be shortened without becomming unreadable.

    ReplyDelete
  2. Hey VivaceVivo, thanks heaps for posting your solution. I hadn't seen that '*' operator on String before - very cool! It seems we've taken fairly different approaches to the problem. I guess the power in the APIs allows us to take many different paths and each of them can still produce succinct, readable code.

    ReplyDelete