Skip to content

Instantly share code, notes, and snippets.

@cscalfani
Last active June 11, 2018 22:59
Show Gist options
  • Select an option

  • Save cscalfani/63c7e90b367eb84134109109ca0717a5 to your computer and use it in GitHub Desktop.

Select an option

Save cscalfani/63c7e90b367eb84134109109ca0717a5 to your computer and use it in GitHub Desktop.
Fun with Function Application (Elm version)

Fun with Function Application (Elm version)

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 x

reducing the noisy parenthesis.

This function is defined in Basics as:

(<|) : (a -> b) -> a -> b
(<|) f x =
    f x

(|>)

The 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

Alternative implementations

|> 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 (<|)

And now time for something completely unexpected

The unexpected

Let's eta-reduce <| by first eliminating the x on both right-hand sides:

(<|) : (a -> b) -> a -> b
(<|) f = f

We 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
(<|) = 
    identity

We 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.

The unexpected and baffling

Since (|>) is just the flip of (<|), we can rewrite it as:

(|>) : a -> (a -> b) -> b
(|>) = 
    flip identity

But 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 x

which 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???

Clarity

The lack of redundant parenthesis can sometimes make type signatures difficult to reason about.

For example, take the type signature for <|:

(<|) : (a -> b) -> a -> b

When 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 identity

All 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 x

Written 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 -> d

and 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 parameter

But 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 x

and identity:

id : a' -> a'
id x = 
    x

We 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 parameter

which 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) -> c

Comparing signatures for flip identity and |> shows that they're equivalent:

flip identity : b' -> (b' -> c) -> c
(|>)          : a  -> (a  -> b) -> b

where b' = a and b = c.

Lessons learned

It's easy but sometimes problematic to think of:

a -> b -> c

as 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 -> f

since 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment