How to Make a Thing in Haskell, Part 5: Wrangling HLint and Reducing etas

As I discussed in my previous post, it’s time to do some string manipulation. In this post, I’ll talk some tool-wrangling I had to do to get that work done, and a rule for writing idiomatic Haskell that I learned on exercism.

The first function I need is one that takes a character c and a string s, and returns a list of each suffix of s that starts with c. A simple Python implementation might be something like

def suffixes_starting_with(character, string):
    while string:
        if string.startswith(character):
            yield string
        string = string[1:]

I’m working on my TDD discipline, so I started in ScoreSpec.hs:

spec = do
    describe "suffixesStartingWith" $ do
        it "returns an empty list for an empty choice" $ property $
            \x -> suffixesStartingWith x "" `shouldbe` []

Now, HLint has something to say about that:

test-suite/ScoreSpec.hs|8 col 8 error| Redundant do Found: do describe "suffixesStartingWith" $ do it "returns an empty list for an empty choice" $ property $ \ x -> suffixesStartingWith x "" `shouldBe` [] Why not: describe "suffixesStartingWith" $ do it "returns an empty list for an empty choice" $ property $ \ x -> suffixesStartingWith x "" `shouldBe` []
test-suite/ScoreSpec.hs|9 col 39 error| Redundant do Found: do it "returns an empty list for an empty choice" $ property $ \ x -> suffixesStartingWith x "" `shouldBe` [] Why not: it "returns an empty list for an empty choice" $ property $ \ x -> suffixesStartingWith x "" `shouldBe` []

That’s a little overcomplicated, but the important bit is: Redundant do Found: do describe "suffixesStartingWith"... Why not try: describe "suffixesStartingWith".... Haskell’s do-notation allows you to chain together operations to be executed in order, but in each of these do blocks, only one thing happens! So HLint is mad about that.

Personally, I don’t care. I’m going to be adding stuff soon, and those dos won’t be redundant anymore. After some digging, I learned how to use pragmas to make HLint ignore particular errors. Adding {-# ANN module "HLint: ignore Redundant do" #-} to the test file, between the imports and the definition of spec, made the error go away. It took a little fiddling to figure out the right place to put it, but below the imports is what did the trick.

eta-Reduction

The definition of suffixesStartingWith winds up being pretty simple. tails from Data.List can generate all the suffixes of the string. This list can be filtered with predicate that returns True only when the input is non-empty and starts with the character we want:

suffixesStartingWith :: Eq a => a -> [a] -> [[a]]
suffixesStartingWith c xs = filter cHead $ tails xs
    where
        cHead []    = False
        cHead (x:_) = x == c

There’s an easy way to make this code more idiomatic. Notice that, after the declaration of xs in the arguments list, it’s only referred to once, as the final argument to tails. Since it’s the final argument, I rewrote this function with what’s called eta-reduction by removing the explicit references to xs:

suffixesStartingWith c = filter cHead . tails
    where
        cHead []    = False
        cHead (x:_) = x == c

This has the same behavior as the previous definition. However, instead of saying “suffixesStartingWith performs these operations on these arguments”, it says “suffixesStartingWith is the composition of these functions”. In this case, it doesn’t change much, but in some cases it can encourage you to write functions as readable pipelines of other functions1 rather than big messes of nested argument-processing junk.

$ and .

One challenge I’ve encountered in learning Haskell has been reasoning about the $ and . operators. They seemed a little bit… I don’t know, mysterious. They’re like & and * in C++: they’re kind of opposites or something, and you add, remove, and move them until your program works and the pain stops.

I’ve gotten through this this by thinking of $ as an executable parenthesis – if you have all the arguments to the functions you’re using, you can use it rather than bracketing off part of the computation:

filter cHead (tails xs) == filter cHead $ tails xs

And I’ve learned to think of the . operator as something like | in shell scripts. So, the eta-reduced version of this function:

filter cHead . tails

reads as “a function that applies (filter cHead) to the result of tails”.

  1. Thanks to exercism user slowthinker for pointing out this article to me.