In an imperative language where "side-effects" are the norm, the term "function" is often used to describe what's more appropriately called a procedure or sub-routine. A set of instruction to be executed when called, and where order of execution and re-evaluation is essential because mutation and other side-effects can change anything from anywhere at any time.
In functional programming, however, the notion of a function is closer to the mathematical sense of the term, where its return value is computed entirely based on its arguments. This is especially true for a "pure" functional language like Elm, which normally does not allow "side-effects". That is, effects that interact with the "outside world" without going through the input arguments or return value. In a pure functional language it does not make sense to have a function that does not take any arguments, because it would always do the same thing, and computing the same value again and again is just wasteful. A function with no arguments is effectively just a value. And a function definition and value binding can therefore be distinguished solely based on whether or not it has any arguments.
But there are also many hybrid programming languages. Most functional languages are hybrids in fact, that allow side-effects but still stick close to the mathematical sense of a function. These languages also typically don't have functions without arguments, but use a special type called unit
, or ()
, which has only one value, also called unit
or ()
, which is used to denote a function that takes no significant input, or which returns nothing significant. Since unit
has only one value, it carries no significant information.
Many functional languages don't even have functions that take multiple arguments either. In Elm and many other languages, a function takes exactly one argument. No more and no less, ever. You might have seen Elm code which appears to have multiple arguments, but that's all an illusion. Or syntax sugar as it's called in language theoretic lingo.
When you see a function definition like this:
add a b = a + b
that actual translates to this:
add = \a -> \b -> a + b
A function that takes an argument a
, then returns another function which takes an argument b
, which does the actual computation and returns the result. This is called currying.
Why do this? Because it makes it very convenient to partially apply functions. You can just leave out the last, or last few, arguments, then instead of an error you get a function back which you can fully apply later to get the result. This enables you to do some really handy things.
Let's look at an example. To fully apple add
from above we'd just do:
add 2 3
The compiler actually parses this as (add 2) 3
, so we've kind of done partial application already, but then immediately applied to another value. But what if we want to add 2
to a whole bunch of things and don't want write add 2
everywhere, because DRY and such? We write a function:
add2ToThings thing =
add 2 thing
(Ok, maybe a little bit contrived, but stay with me)
Partial application allows us to make this even shorter!
add2ToThings =
add 2
You see how that works? add 2
returns a function, and we just give that a name. There have been numerous books written about this marvellous idea in OOP, but they call it "dependency injection" and it's usually slightly more verbose when implemented with OOP techniques.
Anyway, say we have a list of "thing"s, we can get a new list with 2
added to everything by mapping over it like this:
List.map add2ToThings things
But we can do even better! Since add 2
is actually shorter than the name we gave it, we might as well just use it directly:
List.map (add 2) things
Ok, but then say we want to filter
out every value that is exactly 5
. We can actually partially apply infix operators too, but we have to surround the operator in parentheses to make it behave like an ordinary function:
List.filter ((/=) 5) (List.map (add 2) things)
This is starting to look a bit convoluted though, and reads backwards since we filter
after we map
. Fortunately we can use Elm's pipe operator |>
to clean it up a bit:
things
|> List.map (add 2)
|> List.filter ((/=) 5)
The pipe operator was "discovered" because of partial application. Without that it couldn't have been implemented as an ordinary operator, but would have to be implemented as a special syntax rule in the parser. It's implementation is (essentially) just:
x |> f = f x
It takes an arbitrary argument on its left side and a function on its right side, then applies the function to the argument. And because of partial application we can conveniently get a function to pass in on the right side.
So in three lines of ordinary idiomatic Elm code we've used partial application four times. Without that, and currying, we'd have to write something like:
List.filter (\thing -> thing /= 5) (List.map (\thing -> add 2 thing) things)
Or we might want to write it with some variable bindings to make it more readable:
let
add2ToThings thing =
add 2 thing
thingsWith2Added =
List.map add2ToThings things
thingsWith2AddedAndWithout5 =
List.filter (\thing -> thing /= 5) thingWith2Added
in
thingsWith2AddedAndWithout5
And so that's why functional programming is awesome.