tobold.org

correct • elegant • free

△ The $ Operator △

The $ Rule revisited ▻

The $ Rule

In the past, I was confused by the $ operator. It would sometimes appear in other people's programs (never my own, of course, since I didn't understand it), and seemed to be doing something quite magical. Then I devised The $ Rule.

The $ Rule

The $ operator acts as a left parenthesis with an implied right parenthesis at the end of the expression.

That's it. Nothing magical at all. It's just an alternative to using parentheses for grouping.

(As I later discovered, The $ Rule doesn't quite tell the whole story. We will get to that story before the end of this tutorial. But The $ Rule covers the vast majority of uses of the $ operator, so it's a good rule of thumb if you're as puzzled as I was.)

Examples

Let's look at some examples of the $ operator and The $ Rule in practice. Suppose we need a function which will extract a bare file name from a FilePath, and fold it to lower case. We want to write filefold :: FilePath -> String, so that e.g. filefold "/etc/README""readme". Here's our first version, filefold0.hs.

import Data.Char (toLower)

filefold0 fp = map toLower (reverse (takeWhile ('/' /=) (reverse fp)))

Maybe this looks a bit cluttery with all those parentheses, so let's see if the $ operator can help us. At the end of the expression are 3 right parentheses: we can eliminate them by replacing each corresponding left parenthesis with a $ operator. This yields filefold7.hs.

import Data.Char (toLower)

filefold7 fp = map toLower $ reverse $ takeWhile ('/' /=) $ reverse fp

Note that we cannot eliminate the parentheses in ('/' /=). In terms of The $ Rule, this is because the right parenthesis is not at the end of the expression. (More accurately, the parentheses here are a special syntax that creates a section, and are not just for grouping.)

It's a matter of taste whether you prefer filefold0 or filefold7: they are exactly equivalent. In this case, I personally don't think the $ operator does anything to improve clarity. Perhaps we were too greedy: for fewer dollars we can get filefold4.hs, which emphasizes that the transformation takes place in two parts (one of which involves several functions).

import Data.Char (toLower)

filefold4 fp = map toLower $ reverse (takeWhile ('/' /=) (reverse fp))

Altogether, for these 3 nested functions there are 8 ways to split up the work between dollars and parentheses. All are illustrated in filefolds.hs. I think the clearest are filefold0 and filefold4; their duals - filefold7 and filefold3 respectively - also have something to commend them. The remainder are perverse.

import Data.Char (toLower)

filefold0 fp = map toLower (reverse (takeWhile ('/' /=) (reverse fp)))
filefold1 fp = map toLower (reverse (takeWhile ('/' /=) $ reverse fp))
filefold2 fp = map toLower (reverse $ takeWhile ('/' /=) (reverse fp))
filefold3 fp = map toLower (reverse $ takeWhile ('/' /=) $ reverse fp)
filefold4 fp = map toLower $ reverse (takeWhile ('/' /=) (reverse fp))
filefold5 fp = map toLower $ reverse (takeWhile ('/' /=) $ reverse fp)
filefold6 fp = map toLower $ reverse $ takeWhile ('/' /=) (reverse fp)
filefold7 fp = map toLower $ reverse $ takeWhile ('/' /=) $ reverse fp

Of course, there are yet more ways to skin this cat. We can emphasize the two parts of the transformation even further by introducing a new name, as in filefoldn.hs.

import Data.Char (toLower)

filefoldn fp = map toLower f
    where f = reverse (takeWhile ('/' /=) (reverse fp))

Or we could use function composition to eliminate all names (and $ operators), as in filefold.hs. This is the least cluttered version of all, and would, I think, be chosen by most seasoned Haskell programmers. It's definitely not the first version I'd devise, though!

import Data.Char (toLower)

filefold = map toLower . reverse . takeWhile ('/' /=) . reverse

More Examples

Back to the $ operator. The judicious use of the $ operator to separate semantically distinct parts of a complex expression (as in filefold4) seems promising. Consider putStr and friends, which take a string argument. Except in the simplest cases, the argument must be surrounded by parentheses, as in calc0.hs.

main = do
  putStr "Type a number: "; x <- readLn
  putStr "and another: "; y <- readLn
  let s = x + y
  putStrLn (show x ++ " + " ++ show y ++ " = " ++ show s)

A small but definite improvement is to use the $ operator to eliminate the parentheses around the argument to putStrLn. This gives us calc1.hs.

main = do
  putStr "Type a number: "; x <- readLn
  putStr "and another: "; y <- readLn
  let s = x + y
  putStrLn $ show x ++ " + " ++ show y ++ " = " ++ show s

The more complicated the expression, the more compelling the use of the $ operator. There are certain functions that take an IO action as an argument, for example when (and unless). An IO action might be as simple as a call to putStrLn:

when debug (putStrLn "initialization started")
initialize
when debug $ putStrLn "initialization completed"

For a simple case like this, there's not much to choose between parentheses and the $ operator. But an IO action could equally well be a multiline do expression:

when debug $ do
    h <- openFile "debug.out" appendMode
    hPutStrLn h "dump of parse tree"
    hPutStr h (show parseTree)
    hClose h

Now the $ operator is in its element: we could simply surround the entire do expression in parentheses, but it is much neater to avoid a "dangling" right-paren on the last line. This style is common also in GUI programming: here is an example from the gtk2hs package:

renderWithDrawable win $ do
  scale (realToFrac width'  / realToFrac width)
        (realToFrac height' / realToFrac height)
  svgRender svg

Another idiomatic use of the $ operator is in constructors. Rather than Just (x + y), we can write Just $ x + y.

△ The $ Operator △

The $ Rule revisited ▻