Function Types
Why so many arrows? What is going on here?!
It starts to become clearer when you see all the parentheses. For example, it is also valid to write the type of String.repeat
like this:
String.repeat : Int -> (String -> String)
It is a function that takes an Int
and then produces another function. Let’s see this in action:
So conceptually, every function accepts one argument. It may return another function that accepts one argument. Etc. At some point it will stop returning functions.
We could always put the parentheses to indicate that this is what is really happening, but it starts to get pretty unwieldy when you have multiple arguments. It is the same logic behind writing 4 * 2 + 5 * 3
instead of (4 * 2) + (5 * 3)
. It means there is a bit extra to learn, but it is so common that it is worth it.
It is quite common to use the List.map
function in Elm programs:
It takes two arguments: a function and a list. From there it transforms every element in the list with that function. Here are some examples:
List.map String.reverse ["part","are"] == ["trap","era"]
Now remember how String.repeat 4
had type on its own? Well, that means we can say:
List.map (String.repeat 2) ["ha","choo"] == ["haha","choochoo"]
The expression (String.repeat 2)
is a String -> String
function, so we can use it directly. No need to say (\str -> String.repeat 2 str)
.
Elm also uses the convention that the data structure is always the last argument across the ecosystem. This means that functions are usually designed with this possible usage in mind, making this a pretty common technique.
Now it is important to remember that this can be overused! It is convenient and clear sometimes, but I find it is best used in moderation. So I always recommend breaking out top-level helper functions when things get even a little complicated. That way it has a clear name, the arguments are named, and it is easy to test this new helper function. In our example, that means creating:
-- List.map reduplicate ["ha","choo"]
reduplicate : String -> String
reduplicate string =
In other words, if your partial application is getting long, make it a helper function. And if it is multi-line, it should definitely be turned into a top-level helper! This advice applies to using anonymous functions too.
Elm also has a pipe operator that relies on partial application. For example, say we have a sanitize
function for turning user input into integers:
We can rewrite it like this:
-- AFTER
sanitize : String -> Maybe Int
sanitize input =
input
|> String.trim
So in this “pipeline” we pass the input to String.trim
and then that gets passed along to String.toInt
.
This is neat because it allows a “left-to-right” reading that many people like, but pipelines can be overused! When you have three or four steps, the code often gets clearer if you break out a top-level helper function. Now the transformation has a name. The arguments are named. It has a type annotation. It is much more self-documenting that way, and your teammates and your future self will appreciate it! Testing the logic gets easier too.