△ 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.)
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
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 ▻