In this article we will be looking at a concept in FP called functor.
A functor might sound very strange and esoteric but chances are you have used it in some ways. Some of you probably a lot! In this article we will look at what they are and some reasons they are usefull.
Lets start with a definition of a functor and then try to make sense of it. This will not be a comprehensive explanation and we will be glossing over some details for in the interest of brevity but it will hopefully be enough you get you started.
"A functor is a structure with one (or more) values in it that has a function that can transform the value".
This might seem to make little sense now but stay with me. We will start in off in a place many might be familiar: JavaScript!
NB: This is not an official definition but it gets us stared in understanding it.
The functor that are used the most without people even knowing it are probably arrays in JavaScript. One of the ways that you know something is a functor is
if it has the map
function.
The map function on arrays are used to transform (or map 😎) the values inside it to another type (or another instance of the same type):
[1,2,3].map(x => String(x))
// or just:
[1,2,3].map(String)
// outputs: ["1","2","3"]
Map takes one parameter which is a one-parameter function. When sendt (or applied) to map
this function will in turn be called with each of the values in the array giving us a new array containing the new transformed values. In the above example we are transforming the numbers to strings.
(Since we know the parameter to map is a function that takes one parameter and the String
function is a one-parameter function we can just pass in the function directly without a wrapping anonymous function.)
Note that when mapping arrays we always get back an array. The values in it can be transformed into strings, ints, objects or arrays (or any valid JS type) but they will always be inside an Array.
In the statically typed functional language Elm these functional concepts are used all the time but without actually talking about the technical names. Lets look at one of the most used that is not a list/array: Maybe.
Elm does not have null
or undefined
so values that might not be available have to be represented in other ways.
For this the Elm standard library has a type called Maybe and is defined like this:
type Maybe a
= Just a
| Nothing
This defines a type called Maybe which contains a value a
(small letter means any type). It represents to different "states": Just
for when you have a value and Nothing
when you have nothing. ;)
Lets say your system has a profile page where the user reports their age. This can be modelled several ways but we choose to model it as Maybe Int
so its explicit when the user have not supplied the value yet.
(
user = { name = "John Doe", age = Just 42 }
)
Unfortunately we can only get the age as a string from the input field so we will end up with a value of Maybe String
instead. This does not match our type Maybe Int
. But fear not! Map to the rescue! With [toInt][Sting.toInt
] and [map][Maybe.map
] this is easily fixed. ^^,
ageToInt : Maybe String -> Maybe Int
ageToInt maybeString =
Maybe.map String.toInt maybeString
This function takes a paramter maybeString
of type Maybe String
and returns Maybe Int
. Here are some usages and its resulting output:
ageToInt (Just "42")
-- output: Just 42
ageToInt Nothing
-- output: Nothing
ageToInt (Just "not a number")
-- output: Nothing
In the last example we get Nothing
back as String.toInt
fails (it does not know how to convert "not a number" to an Int) and returns Nothing
which in turn Maybe.map
returns us. To use this converted value we can use a case expression:
isOver18 maybeString =
case ageToInt maybeString of
Just age ->
age > 18
Nothing ->
False
What are being mapped are up to the different types or structures in question. The Maybe type is fairly intuitive in that we understand that when we have and instance of Nothing
there is nothing to do and it is relatively simple to just use a case expression to access and transform the value that way. If the type/structure we are working on is more complicated like the RemoteData type the map
function makes our life easier.
type RemoteData e a
= NotAsked
| Loading
| Failure e
| Success a
This type represents and helps us model the states we can have related to an HTTP request. In may situations the interesting part is the Success state and its value. RemoteData.map
makes it easy for us to transform this value without a case statement where we need to handle every possible state of the type.
It is important to note that it is the structure/type that decides what values is given access to in a map
. For lists/array it is all the values in it one by one, for Maybe
it is the Just
case, and for RemoteData
it the Success
case when we know we have git a value from the server. For other structures there are other rules, but often it helps do something to a value only if the right conditions are met without exhausting checking, that is done by the structure/type itself.
There is a lot more to be said about functors but I hope this can get you started in understanding and exploring them more. :)
Fin og lettlest artikkel! Noe språkfusk som jeg har kommentert under, men jeg savner også litt motivasjon rundt hvorfor man trenger functorer. Kanskje du kunne utvidet den første seksjonen med noen av fordelene man får fra functorer, og så la resten av artikkelen være eksempler på bruken?
user = { ...
gjør?r
iString
:)