I'm very new to Haskell and FP in general. I've read many of the writings that describe what currying is, but I haven't found an explanation to how it actually works.
Here is a function: (+) :: a -> (a -> a)
If I do (+) 4 7
, the function takes 4
and returns a function that takes 7
and returns 11
. But what happens to 4
? What does that first function do with 4
? What does (a -> a)
do with 7
?
Things get more confusing when I think about a more complicated function:
max' :: Int -> (Int -> Int)
max' m n | m > n = m
| otherwise = n
what does (Int -> Int)
compare its parameter to? It only takes one parameter, but it needs two to do m > n
.
Haskell, as a functional language, supports higher-order functions (HOFs). In mathematics HOFs are called functionals, but you don't need any mathematics to understand them. In usual imperative programming, like in Java, functions can accept values, like integers and strings, do something with them, and return back a value of some other type.
But what if functions themselves were no different from values, and you could accept a function as an argument or return it from another function? f a b c = a + b - c
is a boring function, it sums a
and b
and then substracts c
. But the function could be more interesting, if we could generalize it, what if we'd want sometimes to sum a
and b
, but sometimes multiply? Or divide by c
instead of subtracting?
Remember, (+)
is just a function of 2 numbers that returns a number, there's nothing special about it, so any function of 2 numbers that returns a number could be in place of it. Writing g a b c = a * b - c
, h a b c = a + b / c
and so on just doesn't cut it for us, we need a general solution, we are programmers after all! Here how it is done in Haskell:
let f g h a b c = a `g` b `h` c in f (*) (/) 2 3 4 -- returns 1.5
And you can return functions too. Below we create a function that accepts a function and an argument and returns another function, which accepts a parameter and returns a result.
let g f n = (\m -> m `f` n); f = g (+) 2 in f 10 -- returns 12
A (\m -> m `f` n)
construct is an anonymous function of 1 argument m
that applies f
to that m
and n
. Basically, when we call g (+) 2
we create a function of one argument, that just adds 2 to whatever it receives. So let f = g (+) 2 in f 10
equals 12 and let f = g (*) 5 in f 5
equals 25.
(See also my explanation of HOFs using Scheme as an example.)
Currying is a technique that transforms a function of several arguments to a function of 1 argument that returns a function of 1 argument that returns a function of 1 argument... until it returns a value. It's easier than it sounds, for example we have a function of 2 arguments, like (+)
.
Now imagine that you could give only 1 argument to it, and it would return a function? You could use this function later to add this 1st argument, now encased in this new function, to something else. E.g.:
f n = (\m -> n - m)
g = f 10
g 8 -- would return 2
g 4 -- would return 6
Guess what, Haskell curries all functions by default. Technically speaking, there are no functions of multiple arguments in Haskell, only functions of one argument, some of which may return new functions of one argument.
It's evident from the types. Write :t (++)
in interpreter, where (++)
is a function that concatenates 2 strings together, it will return (++) :: [a] -> [a] -> [a]
. The type is not [a],[a] -> [a]
, but [a] -> [a] -> [a]
, meaning that (++)
accepts one list and returns a function of type [a] -> [a]
. This new function can accept yet another list, and it will finally return a new list of type [a]
.
That's why function application syntax in Haskell has no parentheses and commas, compare Haskell's f a b c
with Python's or Java's f(a, b, c)
. It's not some weird aesthetic decision, in Haskell function application goes from left to right, so f a b c
is actually (((f a) b) c)
, which makes complete sense, once you know that f
is curried by default.
In types, however, the association is from right to left, so [a] -> [a] -> [a]
is equivalent to [a] -> ([a] -> [a])
. They are the same thing in Haskell, Haskell treats them exactly the same. Which makes sense, because when you apply only one argument, you get back a function of type [a] -> [a]
.
On the other hand, check the type of map
: (a -> b) -> [a] -> [b]
, it receives a function as its first argument, and that's why it has parentheses.
To really hammer down the concept of currying, try to find the types of the following expressions in the interpreter:
(+)
(+) 2
(+) 2 3
map
map (\x -> head x)
map (\x -> head x) ["conscience", "do", "cost"]
map head
map head ["conscience", "do", "cost"]
Now that you understand HOFs and currying, Haskell gives you some syntax to make code shorter. When you call a function with 1 or multiple arguments to get back a function that still accepts arguments, it's called partial application.
You understand already that instead of creating anonymous functions you can just partially apply a function, so instead of writing (\x -> replicate 3 x)
you can just write (replicate 3)
. But what if you want to have a divide (/)
operator instead of replicate
? For infix functions Haskell allows you to partially apply it using either of arguments.
This is called sections: (2/)
is equivalent to (\x -> 2 / x)
and (/2)
is equivalent to (\x -> x / 2)
. With backticks you can take a section of any binary function: (2`elem`)
is equivalent to (\xs -> 2 `elem` xs)
.
But remember, any function is curried by default in Haskell and therefore always accepts one argument, so sections can be actually used with any function: let (+^)
be some weird function that sums 4 arguments, then let (+^) a b c d = a + b + c in (2+^) 3 4 5
returns 14.
Other handy tools to write concise and flexible code are composition and application operator. Composition operator (.)
chains functions together. Application operator ($)
just applies function on the left side to the argument on the right side, so f $ x
is equivalent to f x
. However ($)
has the lowest precedence of all operators, so we can use it to get rid of parentheses: f (g x y)
is equivalent to f $ g x y
.
It is also helpful when we need to apply multiple functions to the same argument: map ($2) [(2+), (10-), (20/)]
would yield [4,8,10]
. (f . g . h) (x + y + z)
, f (g (h (x + y + z)))
, f $ g $ h $ x + y + z
and f . g . h $ x + y + z
are equivalent, but (.)
and ($)
are different things, so read Haskell: difference between . (dot) and $ (dollar sign) and parts from Learn You a Haskell to understand the difference.