A friend of mine recently raised a very interesting question.
If a function takes two arguments, why is its type annotation f: a -> b-> c? Wouldn’t something like f: (a, b) -> c make more sense?
A solid understanding of how Elm treats functions will make the answer clear and allow us to write better code.
Let’s take the following example:
greeting : String -> String -> String gretting hello name = hello ++ ", " ++ name ++ "!"
On the surface, this looks like a function that takes two arguments and returns a result:
greeting "Hello" "DailyDrip" == "Hello, DailyDrip!"
However, functions in Elm always take exactly one argument and return a result (hence the function type annotation syntax).
Passing a single argument to our greeting function returns a new function that takes one argument and returns a string (commented-out output in the examples below comes from elm-repl):
helloGreeting = greeting "Hello" -- <function> : String -> String
Passing an additional argument to this partially applied function will yield the expected result:
helloGreeting "DailyDrip" -- "Hello, DailyDrip!" : String
This shows that the following notation is equivalent:
greeting "Hello" "DailyDrip" == ((greeting "Hello") "DailyDrip")
Parentheses are optional because function evaluation associates to the left by default. Let’s take a look at some examples where partial application is especially useful.
Let’s begin with a simple example of a function that doubles a number. Our first take could be:
double n = n * 2 -- <function> : number -> number
The beautiful thing about Elm is that even operators are functions:
(*) -- <function> : number -> number -> number
We can use partial application to be more concise while achieving the same result:
double = (*) 2 -- <function> : number -> number
Because (*) takes two numbers as arguments, the compiler will infer that our double function takes one number as its sole argument.
While a function to double all elements of a list could be written as:
doubleList list = List.map double list -- <function> : List number -> List number
by applying the same principle, we can do better:
doubleList = List.map double -- <function> : List number -> List number
Now that we know that operators are functions too, we can fully grasp how piping works:
(|>) -- <function> : a -> (a -> b) -> b
It really is no magic: the second argument is a function, which makes it one of the most common use cases for partial application. A good example to demonstrate it is the following function that returns the sum of all deposits:
amountDeposited : List Transaction -> Float amountDeposited list = List.filter (\t -> t.type_ == Deposit) list |> List.map .amount |> List.sum -- rather than the nested equivalent: -- List.sum (List.map .amount (List.filter (\t -> t.type_ == Deposit) list))
There’s one important design feature of Elm that makes all of this possible: data structure is always the last argument. This makes writing better, cleaner, more expressive code using piping and partial application a breeze.