In reading through Chapter 3, I found myself a bit annoyed by the example provided in Step 12: Reading lines from a file.
One of the principle purposes of Chapter 3 is stated to be to make the reader more comfortable with a functional style of programming, and while the example given (which first uses map to find the width of each line, then foldLeft to find the maximum width) is certainly functional, it's a poor argument for why one might want to use functional style; the functional version given is (at least from my naive view) twice as expensive as the corresponding imperative version, requiring two iterations over the list instead of one! This is something that I'd expect to raise the eyebrows of even the most inexperienced first-year student, much less the seasoned developer you are attempting to rally to the functional cause.
A minor annoyance, but one that certainly works against the principle point of making one comfortable with functional style. It gives the impression that lines of code and functional purity, even in this simple case, is preferable to efficiency.
The problem is to run through the list just once would require using a "pattern" in the for expression, which hasn't been introduced yet. But I think that I should add a sentence or two explaining this to comfort folks concerned about impact running through the list twice has on efficiency. I do think that in a script like this, it really is very unlikely to make any perceptible difference in performance most of the time whether you run through the list one or twice, but it is the idea of the thing.
I think I just killed two birds with one stone. On rereading your post, I realized you were talking about something different than the other post I pointed you to. And your complaint is more deserved I think. I do think that people should strive to make things as readable as possible, but refactoring away one of the list traversals I think made the script even more readable. Here's what the book now uses:
val lines = Source.fromFile(args(0)).getLines.toList
val longestLine = lines.reduceLeft( (a, b) => if (a.length > b.length) a else b ) val maxWidth = widthOfLength(longestLine)
for (line <- lines) { val numSpaces = maxWidth - widthOfLength(line) val padding = " " * numSpaces print(padding + line.length +" | "+ line) } } else Console.err.println("Please enter filename")
So one bird I killed was your concern of traversing two lists instead of just one, which made the functional approach look bad compared to the imperative one. But the other bird I killed was the first fellow's concern about calling widthOfLength twice for each line. That's no longer done either. I was able to rewrite the text and everything fit nicely layout-wise, so this is the new example. Thanks again for the feedback, because I think this is better (unless you have another complaint. If so, please tell me soon as this will be going to the printer shortly.).
Kris, I think that while Bill made much better variant than original one, your statement of "the functional version given is ... twice as expensive as the corresponding imperative version" not entirely correct, because book does not provide an imperative version which calculates indentation for each line. To do that using imperative style you would need to traverse list one more time to find a longest line. On the other hand, latest variant shown by Bill uses lines.reduceLeft() which internally also traverse 'lines' list. So, I think that imaginary imperative code which supports indentation and latest functional code pretty much equally effective.
I tried to say: regardless of the code being imperative or functional, a task itself requires two traversals of the list (it might be explicit or hidden inside a function.)