DEV Community

loading...

Partial Application of Functions in Elm

matt24ray profile image matt24ray ・2 min read

This post was originally posted on www.dailydrip.com and it was written by Jan Klimo.

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.

One argument at a time

Let’s take the following example:

greeting : String -> String -> String
gretting hello name = hello ++ ", " ++ name ++ "!"
Enter fullscreen mode Exit fullscreen mode

On the surface, this looks like a function that takes two arguments and returns a result:

greeting "Hello" "DailyDrip" == "Hello, DailyDrip!"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Passing an additional argument to this partially applied function will yield the expected result:

helloGreeting "DailyDrip"
-- "Hello, DailyDrip!" : String
Enter fullscreen mode Exit fullscreen mode

This shows that the following notation is equivalent:

greeting "Hello" "DailyDrip" == ((greeting "Hello") "DailyDrip")
Enter fullscreen mode Exit fullscreen mode

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.

Expressive function definitions

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
Enter fullscreen mode Exit fullscreen mode

The beautiful thing about Elm is that even operators are functions:

(*)
-- <function> : number -> number -> number
Enter fullscreen mode Exit fullscreen mode

We can use partial application to be more concise while achieving the same result:

double = (*) 2
-- <function> : number -> number
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

by applying the same principle, we can do better:

doubleList = List.map double
-- <function> : List number -> List number
Enter fullscreen mode Exit fullscreen mode

Piping

Now that we know that operators are functions too, we can fully grasp how piping works:

(|>)
-- <function> : a -> (a -> b) -> b
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (7)

pic
Editor guide
Collapse
courier10pt profile image
Bob van Hoove • Edited

If a function takes two arguments, why is its type annotation f: a -> b -> c ?

It's by design, which this article covers very well.

Wouldn’t something like f: (a, b) -> c make more sense?

Well, f: (a, b) -> c is a type annotation for a function that takes one argument, a tuple of a, b.

This type annotation makes no assumptions about the concrete types. As such it's hard to tell if it makes sense to require a and b together prior to being applied.

It might seem like a silly question, but thinking about it, functions in Java and C# do exactly this, require all arguments at once in order to be applied. So if that's your background I'd say it's not such a strange question.

...

For my own understanding I prefer working with a more concrete example:

getDistance2d : Int -> Int -> Int -> Int -> Int

This function calculates the distance between 2 coordinates.

You might argue it's easier to deal with:

getDistance2d : (Int, Int) -> (Int, Int) -> Int

The X and Y of a coordinate belong together. So I'd say it's reasonable to apply them in a unified form. You could be more explicit about your intent using a composite type:

getDistance2d: Coordinate -> Coordinate -> Int

And if you really don't want any curried instances:

getDistance2d: (Coordinate, Coordinate) -> Int

But I find it hard to see the benefit of that. The arguments don't belong together like the X and Y components of a Coordinate. So why give up on the luxury of being able to curry?

Disclaimer: I know C# and a bit of Haskell, I googled to check if my examples make sense to an Elm programmer, correct me if I'm wrong.

Collapse
kspeakman profile image
Kasey Speakman • Edited

Nice article!

Slight tangent

In ReasonML f: (a,b) -> c is equivalent to f: a -> b -> c in that you can partially apply the first argument by calling f(aValue).

ReasonML - Function - Currying

When a newcomer to FP learns currying/partial application this way, now there is going to be a stumbling block when transitioning to other functional languages. Because every other function language I'm aware of actually means tuple when the function signature says tuple.

Collapse
eljayadobe profile image
Eljay-Adobe

To express the equivalent greeting as a curried function in JavaScript (ES6):

const greeting = (a) => (b) => { return "" + a + ", " + b + "!"; }
const message = greeting("Hello")("world")

I much prefer the Elm syntax. Much cleaner; obviously influenced by OCaml.

Puts functional programming first, and syntactically path of least resistance.

Collapse
eljayadobe profile image
Eljay-Adobe

One of my buddies pointed out that it would be cleaner using more ES6-isms:

const greeting = (a) => (b) => { return `${a}, ${b}!`; }
Collapse
disfated profile image
disfated

Just to clean this up.

const greeting = a => b => `${a}, ${b}!`
Collapse
rwoodnz profile image
Richard Wood

I'm relatively new in FP but suspect the test for goodness of currying is in the readability.

Your example of the sum of all deposits is a good one because it uses commonly known functions and feels like chaining. These are particularly easy to understand when the same type goes in as out.

Creating the double function is another good one as it simple and very clear what is going on.

I'm wondering though when things are more complex. For example when you see code that shows an unfamiliar more complex function using two arguments and then suddenly it's used with one because a curried function is being created. My early issues with Elm were not being able to clearly see where a function and its parameters ended vs a Tagged value. There may be other situations.