🤔 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!:
- It requires a type constructor/function that has an associated data of type
Result
and produces amsg
type - Additionally, that
Result
type has a constraint: the first type parameter has to be anHttp.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:
e
is not equal toHttp.Error
a
is not equal toMsg
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
- The medium article that helped me grok this
- The krisajenkins/remotedata docs that motivated me to look into this
- The elm-http docs used in the sample code