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)
}
}
Hi Graham,
ReplyDeletenice 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.
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