As you look through packages like
elm/html, you will definitely see functions with multiple arrows. For example:
String.repeat : Int -> String -> String String.join : String -> List String -> String
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.
Fine, but what is the point of this feature in the first place? Why not do
(Int, String) -> String and give all the arguments at once?
It is quite common to use the
List.map function in Elm programs:
List.map : (a -> b) -> List a -> List b
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"]
List.map String.length ["part","are"] == [4,3]
Now remember how
String.repeat 4 had type
String -> String on its own? Well, that means we can say:
List.map (String.repeat 2) ["ha","choo"] == ["haha","choochoo"]
(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 = String.repeat 2 string
This case is really simple, but (1) it is now clearer that I am interested in the linguistic phenomenon known as reduplication and (2) it will be quite easy to add new logic to
reduplicate as my program evolves. Maybe I want shm-reduplication support at some point?
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.
Note: If you are ending up with “too many” functions when you use this advice, I recommend using comments like
-- REDUPLICATIONto give an overview of the next five or ten functions. Old school! I have shown this with
-- VIEWcomments in previous examples, but it is a generic technique that I use in all my code. And if you are worried about files getting too long with this advice, I recommend watching The Life of a File!
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:
-- BEFORE sanitize : String -> Maybe Int sanitize input = String.toInt (String.trim input)
We can rewrite it like this:
-- AFTER sanitize : String -> Maybe Int sanitize input = input |> String.trim |> String.toInt
So in this “pipeline” we pass the input to
String.trim and then that gets passed along to
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.
Note: I personally prefer the
BEFORE, but perhaps that is just because I learned functional programming in languages without pipes!