Quantcast
Channel: Object Mentor Blog: Naming and Body Language in Functional Code
Viewing all articles
Browse latest Browse all 26

Naming and Body Language in Functional Code

$
0
0

I wrote a blog the other day about functional refactoring and I had what I thought was a good example:

absPosition :: Buffer -> Int
absPosition (Buffer (x, y) contents) = x + lenPreviousLines contents
  where lenPreviousLines = 
    foldr ((+).length) 0 . take y . terminatedLines

Almost immediately, I saw replies on a couple of forums (including this one) which pointed out that I could’ve written the code this way:

absPosition (Buffer (x, y) contents) = x + lenPreviousLines contents
  where lenPreviousLines = 
    sum . map length . take y . terminatedLines

It’s funny, I thought of using sum instead of foldr back when I was using Haskell’s line function. The code I had looked like this:

absPosition (Buffer (x, y) contents) = x + lenPreviousLines contents
  where lenPreviousLines = 
    foldr ((+).((+1).length)) 0 . take y . lines

But, I realized that the code wasn’t in great shape for sum, so I created terminatedLines, used it and promptly forgot to do the refactoring I set out to do.

terminatedLines :: String -> [String]
terminatedLines = map (++ "\n") . lines

From an imperative point of view, terminatedLines looks a bit silly: What?? You’re going to append a newline to each line in a list of lines you just created just so that you can count it?? But, I suspect that it isn’t that bad. The evaluator pulls values from each line and as it reaches the end of one it should just put a newline at the end of it. If I’m wrong about this, please let me know.

In any case, I agree that the code looks better with sum that it does with foldr (+) 0. The big question is – should we refactor any more?

Someone with the handle sterl suggested a very cool trick. I could drop the where clause like this:

absPosition (Buffer (x, y) contents) = 
  x + (sum . map length . take y . terminatedLines) contents

And then move on to this:

absPosition (Buffer (x, y) contents) = 
  sum . (x:) . map length . take y . terminatedLines $ contents

What’s going on here? Well as sterl put it, we’re summing anyway so why not prepend the x onto the list that we are already summing?

Part of me likes this and part of me doesn’t. One the one hand, it’s brief, but on the other hand, the code isn’t telling us why it is doing what it is doing anymore. In the original code, there is an algorithm:

To get the absolute position, add the x position of the location to the sum of the lengths of all of the previous lines.

In the new code, the algorithm is:

To get the absolute position, sum the current x position with the lengths of all of the previous lines.

Wait, that’s sort of the same, isn’t it?

This example points to a fundamental dilemma that I have with naming in Haskell. I’m used to introducing names in lower-level languages to bridge the gap between intention and mechanism, but what happens when your mechanism is so high-level that it can speak for itself? Maybe we don’t need names as much?

Now, I know as I write this that someone is going to look at this as an extreme statement. It isn’t. Names are useful, and indispensable, but really they are only one of several ways of communicating meaning. In each case, we have to pick the right tool for the job. With Haskell, I think that programmers communicate with structure as much as they communicate with names. It’s the body-language of their code.


Viewing all articles
Browse latest Browse all 26

Latest Images

Trending Articles





Latest Images