🤔 Functions in my types?

I'm a sucker for strict functional programming languages such as Elm and a total newbie in this field. The following is a post about something that I keep forgetting (or maybe I never totally understood!) and this is my attempt to finally have it sink it into my mind and (hopefully) help you understand it as well.

Presenting the problem#

The following code is part of an application I'm building just for fun. While using krisajenkins/remotedata library, I had to copy/paste a sample code from the docs, because I would have never come up with it myself:

import RemoteData exposing (RemoteData, WebData)
type alias Data =
{ name : String, url : String }
type alias Pokemon =
{ name : String
, abilities : List Data
, moves : List Data
}
type Msg
= GotPokemon (WebData Pokemon)
fetchPokemon : Cmd Msg
fetchPokemon =
Http.get
{ url = "https://pokeapi.co/api/v2/pokemon/1"
, expect = Http.expectJson
(RemoteData.fromResult >> GotPokemon)
pokeDecoder -- details of this decoder aren't important
}

This part specifically (line 20 from the code above):

(RemoteData.fromResult >> GotPokemon)

Why do I need to pass it like that?, also the docs for RemoteData.fromResult says:

Convert a Result, probably produced from elm-http, to a RemoteData value.

But elm-http knows nothing about RemoteData type, so shouldn't it be the other way around? Shouldn't the RemoteData type be transformed before passing it into this elm-http function?.

That was my naive and totally wrong question 🤓. So, What's the deal then?

In order to understand what is going on, let's start with the basics:

Functions in Elm#

Given the following sum function:

sum: Int -> Int -> Int
sum x y = x + y

Yep, as you might have guessed, it sums two integers, represented by the type: Int. But that is not exciting at all!, so let's focus on the important bits:

Type annotations

Above the implementation of sum, there are a bunch of arrows -> and Int's, which are used to state that the function receives two parameters of type Int and returns anInt (well, its actually more deeper than that, but for practical purposes, my shallow explanation works), so these are Type annotations, which helps a developer to understand what the function expects and returns, and I say it helps the developer to understand because they are totally optional!, the Elm compiler can easily figure all this out by itself.

Types#

In Elm you can declare your own custom types too!, and is dead simple, for example:

type Color = Blue | Yellow

Let's inspect a little bit these examples in the repl elm repl, just copy and paste the sum code (including the type annotation), then type sum:

> sum
<function> : Int -> Int -> Int

It shows the type annotation as the resulting value, nothing amusing here. Now let's do the same with the Color type, and type Blue

> Blue : Color

(If you typed Color then you got an error message, well let's leave that for another post 😉.)

If you squint your eyes at Blue : Color, it kinda looks like a Type Annotation, just like functions, right?. Try typing a built-in value such as True or "Hello World"

> True
True : Bool
> "Hello World"
"Hello World" : String

By now, you have probably realized what's going on, all these are just values, and all Elm is doing is telling us what Type they are, nothing fancy here.

Let's level up a bit and add a new Color value...with a twist:

> type Color = Blue | Yellow | RGB Int Int Int
> RGB
<function> : Int -> Int -> Int -> Color
>

Whoah! What happened? it really looks like a function type signature for RGB!

Well, it actually is, so in Elm, you can have types with associated data, in this case the RGB value is associated with three Int types

Let's give values to it:

> RGB 3 4 7
RGB 3 4 7 : Color

So now we have the expected value: Type format. What we did previously was to partially apply the RGB value with no parameters, from which we cannot really produce a Color type. That function is called Type Constructor and it was auto generated by the Elm compiler.

Now let's go back to the original issue at hand, here's the type we're working with:

import RemoteData exposing (RemoteData, WebData)
type Msg
= GotPokemon (WebData Pokemon)
> GotPokemon -- I just typed this to see the type constructor
<function> : WebData Pokemon -> Msg

By this point you probably realized that here is exactly the same thing as we saw before, but with more hops 🐰.

Now, let's inspect the type annotation of expectJson:

expectJson : (Result Error a -> msg) -> Decoder a -> Expect msg

So expectJson expects (no pun intended!) the first argument to be something like Result Error a -> msg , but I don't have a Result in my types!, all I have is a WebData type from RemoteData library.

All right, let's look at WebData definition from the docs:

type alias WebData a = RemoteData Http.Error a

Well, that looks simple tho, so if I expand that alias what I really have is:

type Msg
= GotPokemon (RemoteData Http.Error Pokemon)

Note: I'm explicitly using Http.Error instead of just Error for readability purposes, its the same type, the docs says so!.

Diving deeper into the Type

Let's zoom into the expectJson's first argument : (Result Error a -> msg) ; it is telling us a lot!:

  1. It requires a type constructor/function that has an associated data of type Result and produces a msg type
  2. Additionally, that Result type has a constraint: the first type parameter has to be an Http.Error type

Do we have any type constructor that satisfies those two requirements? Let's see, our Msg type has a type constructor with a single parameter:

GotPokemon
<function> : WebData Pokemon -> Msg

From there, let's expand WebData type alias to see what really is so we can see better:

GotPokemon
<function> : RemoteData Http.Error Pokemon -> Msg

All right cool, now it has a kinda similar shape and it satisfies the Http.Error constraint, but is not the same type: RemoteData is not the same as Result, so close!.

Well this type incompatibility is the actual problem that we need to solve!, what we need is to pass something like a type constructor that knows how to receive a Result Http.Error a and produce our msg, which in this case is Msg (note the capital M!)

So this conversion can be achieved by using fromResult : Result e a -> RemoteData e a, but we cannot just calling it on the spot, we need to delay the transformation to happen whenever expectJson needs it. So what we need is to pass this conversion function that deals with this the mismatching of types and produce the Msg type.

Compose me a poem 💕

To create this conversion function, we leverage to something called function composition, that says:

If functions f: a -> b and g: b -> c then g(f): a -> c, which essentially connects two functions; the output of f into the input of g, obtaining a new function g(f) that receives a and produces a result c, neat right?

So let's compose:

-- Let's inspect our f and g functions 👀
-- This is our f function
> RemoteData.fromResult
<function> : Result e a -> RemoteData e a
-- This is our g function
> GotPokemon
-- Here I'm using the expanded form of the type alias
<function> : RemoteData Http.Error Pokemon -> Msg

Cool, it looks like we can do the composition now, by connecting f's RemoteData e a and g's RemoteData Http.Error Pokemon, or at the very least, they have the same RemoteData type.

But while the type matches, the type parameters do not match:

  1. e is not equal to Http.Error
  2. a is not equal to Msg

Boo, how do we deal with this? well the thing is that RemoteData.fromResult uses polymorphic type parameters, a neat way to create generic functions that will work with any custom type. Well if that is the case, then how come we cannot use it as-is with my custom type? What gives?.

Remember that Elm is a statically typed language, we cannot call functions on the fly and figure out the types at runtime. We need to unify the types at compile time, essentially telling the Elm compiler that we want to use concrete types so that generic function will match the types of GotPokemon. In other words whenever you want to use functions with polymorphic types, the caller of the those functions declare the actual types of those polymorphic type parameters (unless the caller is just handing over those parameters).

How do we do this? Well, you are in luck! Elm compiler is so smart, that it can figure it out by itself, by just looking at the function implementation (this is the part where Elm has a dynamic feeling to it, thanks to type inference):

> converter result = GotPokemon (RemoteData.fromResult result)
<function> Result Http.Error Pokemon -> Msg

And now it compiles with concrete types and is ready to be used in the expectJson function!.

Bonus level

This part are just tricks of the same thing it was discussed above. In Elm there are two infix functions that are used for function composition:

> (>>) -- composition from the left
<function> : (a -> b) -> (b -> c) -> a -> c
> (<<) -- composition from the right
<function> : (b -> c) -> (a -> b) -> a -> c

We can refactor our previous code further by leveraging composition from the left >> operator:

> converter result = (RemoteData.fromResult >> GotPokemon) result
<function> Result Http.Error Pokemon -> Msg

Looks tidier right? Believe it or not, we can further refactor this by leveraging to Point Free Notation (this is a tale for another post, I promise!), like so:

> converter = RemoteData.fromResult >> GotPokemon
<function> Result Http.Error Pokemon -> Msg

The type annotation remains unchanged, meaning that all these functions are equivalent, and the last one looks so elegant that it has to be taken to the fanciest of parties 🎉.

Takeaway#

Before writing this post I used to think about Types and its values, which is totally correct, but thanks to my biases with imperative languages, the term values was a bit distorted. So instead, I simply see them as Type constructors or just functions 😉, which again, is also correct because in Elm, functions are also values 🤯.

Further Reading