Function application operators in Elm make programming more understandable and help reduce parenthesis.
The application operator, <|, can be used to reduce the need for parenthesis. The following:
f (g x)can be rewritten as:
f <| g xreducing the noisy parenthesis.
This function is defined in Basics as:
(<|) : (a -> b) -> a -> b
(<|) f x =
f xThe reverse application operator, |>, is even more useful allowing us to write code as a Data Flow.
The following:
map ((*) 10) <| filter (flip (<) 2) [1, 2, 3, 4, 5]can be rewritten as:
[ 1, 2, 3, 4, 5 ]
|> List.filter (flip (<) 2)
|> List.map ((*) 10)making the code easier to follow.
This function is defined in Basics as:
(|>) : a -> (a -> b) -> b
(|>) x f =
f x|> is just the flip of <| as can be seen by the type signatures.
So let's rewrite |> in terms of <|:
(|>) : a -> (a -> b) -> b
(|>) =
flip (<|)Let's eta-reduce <| by first eliminating the x on both right-hand sides:
(<|) : (a -> b) -> a -> b
(<|) f = fWe recognize our reduced function as just identity, takes an f and returns an f. So let's refactor with that fact:
(<|) : (a -> b) -> a -> b
(<|) =
identityWe could've also infered this by just considered the type signature and by added in the implied right-hand parenthesis giving us:
(a -> b) -> (a -> b)which is equivalent to a -> a the type signature for identity.
One very important thing to keep in mind is that the type signature for <| keeps it from being used in place of identity in your code.
You could say the <| is identity for functions ONLY.
The fact that <| is just identity was unexpected but makes sense.
Since (|>) is just the flip of (<|), we can rewrite it as:
(|>) : a -> (a -> b) -> b
(|>) =
flip identityBut wait! This is both unexpected and, at first glance, doesn't make sense.
The baffling thing is that flip has the following implementation in Basics:
flip : (a -> b -> c) -> b -> a -> c
flip f x y =
f y xwhich says that flip takes as its first parameter a function of 2 parameters.
But identity takes only 1 parameter! So then how can you flip a function with only 1 parameter???
The lack of redundant parenthesis can sometimes make type signatures difficult to reason about.
For example, take the type signature for <|:
(<|) : (a -> b) -> a -> bWhen we added in the redundant, right-side parenthesis:
(<|) : (a -> b) -> (a -> b)it instantly became obvious that <| is just identity.
But our confusion above is with |>:
(|>) : a -> (a -> b) -> b
(|>) =
flip identityAll of our substitutions that got us here were valid and this code compiles but it's just not intuitive as to how flip combines with identity.
So once again, let's add in the implied right-assosiative parenthesis to the first parameter of flip:
flip : (a -> (b -> c)) -> b -> a -> c
flip f x y =
f y xWritten this way, which is equivalent to without the parenthesis, it appears that the first parameter is a function of 1 parameter!
This can be confusing even if you've seen this before. Coming from non-curried languages, it's easy to think in terms of multiple parameters. But that's where our thinking can go astray.
For example, we can take the following:
a -> b -> c -> dand just by adding parenthesis, make it look like the function takes 1, 2, or 3 parameters:
a -> b -> c -> d -- 3 parameters
a -> b -> (c -> d) -- 2 parameters
a -> (b -> c -> d) -- 1 parameterBut all functions ONLY take 1 parameter!!! That's what Currying is all about.
So the objection about passing identity to flip because the first parameter of flip takes 2 parameters is wrong!
With this in mind, let's look at flip with the extra parenthesis:
flip : (a -> (b -> c)) -> b -> a -> c
flip f x y =
f y xand identity:
id : a' -> a'
id x =
xWe can apply flip to identity and figure out its type signature.
The first parameter of flip is identity which means that the types must be equivalent and line up:
a' -> a' -- identity
a -> (b -> c) -- flip's first parameterwhich means:
a' = a
a' = (b -> c)which then means:
a = (b -> c)since they both equal a'.
Now we're ready to determine the type signature for flip identity.
First let's return to the type signature for flip and remove the first parameter since we have given it identity as the first parameter AND let's also replace a with (b -> c):
flip : (a -> (b -> c)) -> b -> a -> c
flip identity : b -> (b -> c) -> cComparing signatures for flip identity and |> shows that they're equivalent:
flip identity : b' -> (b' -> c) -> c
(|>) : a -> (a -> b) -> bwhere b' = a and b = c.
It's easy but sometimes problematic to think of:
a -> b -> cas a function of 2 parameters when in fact it's really:
a -> (b -> c)which is clearly a function of only 1 parameter.
As is:
a -> b -> c -> d -> e -> fsince it's really:
a -> (b -> (c -> (d -> (e -> f))))which is function of only 1 parameter also.
Ask a Elm programmer, what does flipping identity look like and they'll probably scratch their heads and think "Why would you need to flip identity?" or "Wait, you can't do that! Flip swaps the first 2 parameters and identity only has 1."
I know that I did.
But as we've seen, you can. It's the way we typically think about flip that's wrong.