19

I started learning Haskell and I encountered a problem I can't just understand. I've got a method used to find value from a list of key-value list (from this page):

let findKey key xs = snd . head . filter (\(k,v) -> key == k) $ xs  

I tried fiddling with a bit and decided to get rid of $ sign in this way:

let findKey key xs = snd . head . filter (\(k,v) -> key == k) ( xs )

However, it doesn't even parse (filter applied to too many argumens error). I've read that $ sign is used to simply replace parenthesis and I can't figure out why this simple change of code is bad. Could someone explain it to me?

TheMP
  • 8,257
  • 9
  • 44
  • 73
  • 3
    possible duplicate of [Haskell: difference between . (dot) and $ (dollar sign)](http://stackoverflow.com/questions/940382/haskell-difference-between-dot-and-dollar-sign) – cdk Jun 17 '14 at 19:48

5 Answers5

31

The infix operator ($) is just "function application". In other words

 f   x     -- and
 f $ x

are the same. Since in Haskell parentheses are only used to disambiguate precedence (and for tuple notation and a few other minor places, see comments) we can also write the above in a few other ways

 f     x
 f  $  x
(f)    x
 f    (x)
(f)   (x)    -- and even
(f) $ (x)

In every case, the above expressions denote the same thing: "apply the function f to the argument x".

So why have all this syntax? ($) is useful for two reasons

  1. It has really low precedence so it can stand in for a lot of parentheses sometimes
  2. It's nice to have an explicit name for the action of function application

In the first case, consider the following deeply right-nested function application

f (g (h (i (j x))))

It can be a little difficult to read this and a little difficult to know you have the right number of parentheses. However, it's "just" a bunch of applications so there ought to be a representation of this phrase using ($). Indeed there is

 f $ g $ h $ i $ j $ x

Some people find this easier to read. More modern style also incorporates (.) in order to emphasize that the whole left side of this phrase is just a composed pipeline of functions

 f . g . h . i . j $ x

And this phrase is, as we saw above, identical to

(f . g . h . i . j)  x

which is sometimes nicer to read.


There are also times when we want to be able to pass around the idea of function application. For instance, if we have a list of functions

lof :: [Int -> Int]
lof = [ (+1), (subtract 1), (*2) ]

we might want to map application by a value over them, for instance apply the number 4 to each function

> map (\fun -> fun 4) lof
[ 5, 3, 8 ]

But since this is just function application, we can also use section syntax over ($) to be a bit more explicit

> map ($ 4) lof
[ 5, 3, 8 ]
Community
  • 1
  • 1
J. Abrahamson
  • 72,246
  • 9
  • 135
  • 180
17

The operator $ has the lowest priority, so

snd . head . filter (\(k,v) -> key == k) $ xs

is read as

(snd . head . filter (\(k,v) -> key == k)) xs

while your second expression is rather

snd . head . ( filter (\(k,v) -> key == k) xs )
bereal
  • 32,519
  • 6
  • 58
  • 104
7

The $ sign isn't magic syntax for replacing parentheses. It's an ordinary infix operator, in every way that an operator like + is.

Putting brackets around a single name like ( xs ) is always equivalent to just xs1. So if that's what the $ did, then you'd get the same error either way.

Try to imagine what would happen if you had some other operator you're familiar with there, such as +:

let findKey key xs = snd . head . filter (\(k,v) -> key == k) + xs

Ignore the fact that + works on numbers so this makes no sense, and just think about the structure of the expression; which terms are being recognised as functions, and which terms are being passed to them as arguments.

In fact, using + there does actually parse and typecheck successfully! (It gives you a function with nonsense type class constraints, but if you fulfill them it does mean something). Lets walk through how the infix operators are resolved:

let findKey key xs = snd . head . filter (\(k,v) -> key == k) + xs

The highest precedence thing is always normal function application (just writing terms next to each other, with no infix operators involved). There's only one example of that here, filter applied to the lambda definition. That gets "resolved", and becomes a single sub-expression as far as parsing the rest of the operators is concerned:

let findKey key xs
      = let filterExp = filter (\(k,v) -> key == k)
        in  snd . head . fileterExp + xs

The next highest precedence thing is the . operator. We've got several to choose from here, all with the same precedence. The . is right associative, so we take the rightmost one first (but it wouldn't actually change the result whichever one we pick, because the meaning of the . is an associative operation, but the parser has no way of knowing that):

let findKey key xs
      = let filterExp = filter (\(k,v) -> key == k)
            dotExp1 = head . filterExp
        in  snd . dotExp1 + xs

Note that the . grabbed the terms immediately to its left and right. This is why precedence is so important. There's still a . left, which is still higher precedence than +, so it goes next:

let findKey key xs
      = let filterExp = filter (\(k,v) -> key == k)
            dotExp1 = head . filterExp
            dotExp2 = snd . dotExp1
        in  dotExp2 + xs

And we're done! + has lowest precedence of the operators here, so it gets its arguments last, and ends up being the top-most call in the whole expression. Note that the + being low precedence prevented the xs being "claimed" as an argument by any of the higher precedence applications to the left. And if any of them had been lower precedence, they would have ended up taking the whole expression dotExp2 + xs as an argument, so they still couldn't have got to xs; putting an infix operator before xs (any infix operator) prevents it from being claimed as an argument by anything to the left.

This is in fact exactly the same way that $ is parsed in this expression, because . and $ happen to have the same relative precedence that . and + have; $ is designed to have extremely low precedence, so it will work this way with almost any other operators involved to the left and right.

If we don't put an infix operator between the filter call and xs, then this is what happens:

let findKey key xs = snd . head . filter (\(k,v) -> key == k) xs

Normal function application goes first. Here we've got 3 terms simply next to each other: filter, (\(k,v) -> key == k), and xs. Function application is left associative, so we take the leftmost pair first:

let findKey key xs
      = let filterExp1 = filter (\(k,v) -> key == k)
        in  snd . head . filterExp1 xs

There's still another normal application left, which is still higher precedence than the ., so we do that:

let findKey key xs
      = let filterExp1 = filter (\(k,v) -> key == k)
            filterExp2 = filterExp1 xs
        in  snd . head . filterExp2

Now the first dot:

let findKey key xs
      = let filterExp1 = filter (\(k,v) -> key == k)
            filterExp2 = filterExp1 xs
            dotExp = head . filterExp2
        in  snd . dotExp

And we're done, the top-most call in the whole expression this time was the left-most . operator. This time xs got sucked in as a second argument to filter; this is sort-of where we want it since filter does take two arguments, but it leaves the result of filter in a function composition chain, and filter applied to two arguments can't return a function. What we wanted was to apply it to one argument to give a function, have that function be part of the function composition chain, and then apply that entire function to xs.

With $ there, the final form mirrors that when we used +:

let findKey key xs
      = let filterExp = filter (\(k,v) -> key == k)
            dotExp1 = head . filterExp
            dotExp2 = snd . dotExp1
        in  dotExp2 $ xs

It's parsed exactly the same way as when we had +, so the only difference is that where + means "add my left argument to my right argument", $ means "apply my left argument as a function to my right argument". Which is what we wanted to happen! Huzzah!

TLDR: The bad news is that $ doesn't work by just wrapping parentheses; it's more complicated than that. The good news is that if you understand how Haskell resolves expressions involving infix operators, then you understand how $ works. There is nothing at all special about it as far as the language is concerned; it's an ordinary operator you could define yourself if it didn't exist.


1 Parenthesising an operator like (+) also just gives you exactly the same function denoted by +, but now it doesn't have the special infix syntax, so this does affect how things are parsed in this case. Not so with ( xs ) where it's just a name inside.

Ben
  • 68,572
  • 20
  • 126
  • 174
4

"$ sign is used to simply replace parenthesis" is pretty much correct – however, it effectively parenthesises everything to both sides! So

snd . head . filter (\(k,v) -> key == k) $ xs

is effectively

( snd . head . filter (\(k,v) -> key == k) ) ( xs )

Of course, the parens around xs are unneeded here (that's an "atom" anyway), so the relevant ones are in this case around the left side. Indeed that happens often in Haskell, because the general philosophy is to think about functions as abstract entities as much as possible, rather than the particular values involved when applying the function to some argument. Your definition could also be written

let findKey key xs' = let filter (\(k,v) -> key == k) $ xs
                          x0 = head xs'
                          v0 = snd x0
                      in v0

That would be extremely explicit, but all those intermediate values aren't really interesting. So we prefer to simply chain the functions together "point-free", with .. That often gets us rid of a lot of boilerplate, indeed for your definition the following can be done:

  1. η-reduction of the xs. That argument just passed on to the chain of functions, so we might as well say "findKey key" is that chain, with whatever argument you supply"!

        findKey key = snd . head . filter (\(k,v) -> key == k)
    
  2. Next, we can avoid this explicit lambda: \(k,v) -> k is simply the fst function. You then need to postcompose comparison with key.

        findKey key = snd . head . filter ((key==) . fst)
    
  3. I'd stop here, since too much point-free is pointless and unreadable. But you could go on: there's new parens now around the argument to key, we can again get rid of those with $. But careful:

        "findKey key = snd . head . filter $ (key==) . fst"
    

    is not right, because the $ would again parenthesise both sides, yet (snd . head . filter) is not well-typed. Actually snd . head should only come after both arguments to filter. A possible way to do such a post-post-composition is using the function functor:

        findKey key = fmap (snd . head) . filter $ (key==) . fst
    

    ...we could go on even further and get rid also of the key variable, but it wouldn't look nice. I think you've got the point...

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
4

Other answers have commented in detail on how ($) can replace parentheses, since ($) was defined as an application operator with the right precedence.

I'd like to add that GHC, in order to make it possible to replace parentheses with ($), uses some more magic under the hood than what can be seen from the definition of ($). When higher rank functions are involved, the type system can cope with higher-rank arguments when passed through standard application (as in f x), but not when passed though an application operator (as in f $ x). To overcome this problem, GHC handles ($) in a special way in the type system. Indeed, the following code shows that if we define and use our own application operator ($$), the type system does not apply the same magic handling.

{-# LANGUAGE RankNTypes #-}

-- A higher rank function
higherRank :: (forall a. a -> a -> a) -> (Int, Char)
higherRank f =  (f 3 4, f 'a' 'b')

-- Standard application
test0 :: (Int, Char)
test0 = higherRank const     -- evaluates to (3,'a')

-- Application via the ($) operator
test1 :: (Int, Char)
test1 = higherRank $ const   -- again (3, 'a')

-- A redefinition of ($)
infixr 0 $$
($$) :: (a -> b) -> a -> b
($$) = ($)

test2 :: (Int, Char)
test2 = higherRank $$ const  -- Type error (!)
-- Couldn't match expected type `forall a. a -> a -> a'
--             with actual type `a0 -> b0 -> a0'
chi
  • 111,837
  • 3
  • 133
  • 218