Her name is Funkty, and she’ll never regret that tattoo.

My Little Functor

Mapping is magic!

Michel Belleville
Published in
5 min readMar 22, 2022

--

wat> Who’s a good little functor? That’s a good little functor…

me> Er… what are you doing exactly?

wat> I’m training my functor.

me> Is that a euphemism for some weird ducky thing involving functional programming?

wat> Yes. You can watch if you want 😉…

me> Don’t mind me if I do 😅

wat> So, here’s FutureBox ; it’s a new type that I made. Remember, a functor is a context to wrap around a value… let’s see if you can tell what the context is here:

type FutureBox a
= Done a
| StillWorking
| Failed String

me> Ok. Looks a bit like Maybe, except there are two Nothing-like states… so the context here is possible failure with Failed String, or, well, something to indicate we’re still working on getting the result I guess?

wat> Precisely. Say we want to display something that needs some time to compute. This type tells us that either we’ve got it already (Done a), or we’re StillWorking on it, or maybe we Failed, and we’ve got a String that explains why.

me> OK 😃 Now what?

wat> Now, how do we make it a functor?

me> Why would we make it a functor?

wat> Because we want the ability to change its content when it’s Done without having to write yet another case .. of. So, how do we do it?

me> Well, memory serves, we need a map function:

FutureBox.map : (a -> b) -> FutureBox a -> FutureBox b

wat> Good. Now let’s write it…

me> Ok. How about this:

FutureBox.map : (a -> b) -> FutureBox a -> FutureBox b
FutureBox.map transformation box =
case box of
Done a->
Done (transformation a)

wat> Well, you’re transforming a when it’s Done and re-wrapping it in the Done context, which is great… but the compiler won’t appreciate that you haven’t told it how to deal with the other cases.

me> Of course. Let me fix it…

FutureBox.map : (a -> b) -> FutureBox a -> FutureBox b
FutureBox.map transformation box =
case box of
Done a ->
Done (transformation a)
_ ->
StillWorking

me> …there. I fixed it.

wat> Fixed it have you? 😈 Let’s see how well your fix performs:

> Done 3 |> FutureBox.map ((+) 2)
Done 5 -- so far, so good
> StillWorking |> FutureBox.map ((+) 2)
StillWorking -- also good
> Failed "Ooops" |> FutureBox.map ((+) 2)
StillWorking -- WHAT?!?

wat> See the problem? Your implementation changed the context from Failed "Ooops" to StillWorking in the third example.

me> And that’s a problem… why?

wat> Because a well-behaved map should never change the context itself, only the type parameter. That’s part of the functor “contract” if you will. So, when the context is StillWorking, it should stay StillWorking, and when it’s Failed "Ooops", it should still be Failed "Ooops", and not StillWorking. Nor Failed "Hihi" either.

me> Ok. That sounds kind of logical. Let me try and fix it better:

FutureBox.map : (a -> b) -> FutureBox a -> FutureBox b
FutureBox.map transformation box =
case box of
Done a ->
Done (transformation a)
StillWorking ->
StillWorking
Failed reason ->
Failed reason

wat> Very nice! You’ve thoughtfully kept the reason for the Failed case too. Very nice indeed.

me> I couldn’t use transformation on it since it’s a String and not whatever a is be when it’s called called...

wat> Good. Now let’s see how it fares:

> Done 3 |> map ((+) 2)
Done 5 -- so far, so good
> StillWorking |> map ((+) 2)
StillWorking -- also good
> Failed "Ooops" |> map ((+) 2)
Failed "Ooops" -- very good :)

wat> Perfect. Now, you know what could be useful when we design our functors? A standard way to tell whether they’re behaving as they should…

me> And that standard way… exists?

wat> Yes it does! There are functor laws, see, that a well-behaved functor has to obey. And the nice thing is, they’re kinda translatable as code.

me> Argh. I guess there are plenty of these, aren’t they?

wat> Oh, yes! Two.

me> Oh. Well. If there aren’t that many, I guess they are complicated, aren’t they?

wat> Very. See for yourself:

-- say we have a type F with one type parameter...
type F a
-- ...we'll use the `identity` function, it returns its param...
identity : a -> a
identity a = a
-- ...of course our functor F has a `map` function:
F.map : (a -> b) -> F a -> F b
-- The first law states that applying the identity function to the
-- value "inside" our functor instance through the functor's map
-- function is the same as applying the identity function to our
-- functor instance itself.
F.map identity functorValue == identity functorValue-- That is to say, if we map with a function that changes nothing
-- neither the value nor the context should change.

me> Well, that’s a rather simple law…

wat> One our first map implementation broke though.

me> Yes. Now that second law?

wat> It’s a bit more difficult:

-- The second law states that mapping one function then another
-- on a functor is the same as mapping the composite of those two
-- functions, so:
f : a -> b
g : b -> c
F.map (f >> g) functorValue ==
functorValue
|> F.map f
|> F.map g

me> So… for example:

-- Say we create a CountingBox, a box that counts every time the
-- function map has been used to change its value...
type CountingBox a = CountingBox Int aCountingBox.map : (a -> b) -> CountingBox a -> CountingBox b
CountingBox.map transformation box =
case box of
CountingBox count a ->
let
ta = transformation a
in
if ta /= a then
-- only increment when the value changed
CountingBox (count + 1) ta
else
CountingBox count a

wat> Yup, this is not a well-behaved functor, it breaks the second law:

addOne : Int -> Int
addOne n = n + 1
timesTwo : Int -> Int
timesTwo n = n * 2
CountingBox 0 5
|> CountingBox.map (addOne >> timesTwo)
== CountingBox 1 12 -- we mapped once, changing the value once
CountingBox 0 5
|> CountingBox.map addOne
|> CountingBox.map timesTwo
== CountingBox 2 12 -- we mapped twice, changing the value twice

me> It respects the first law though:

identity CountingBox 0 1
== CountingBox 0 1
CountingBox 0 1
|> CountingBox.map identity
== CountingBox 0 1
-- identity did not change the value, so no increment was made

wat> Exactly 👏 Now, provided your functor respects both laws, it’ll behave like a well educated functor, in a very predictable way for someone who knows what functors are… which in turn will make your codebase more readable.

me> Nice. Well, now that we’ve talked about functor laws… I bet you’re going to tell me Monads have them too

wat> Hehe, you know me too well. Another time though…

--

--