My Little Functor
Mapping is magic!
wat> Who’s a good little functor? That’s a good little functor…
me> Er… what are you doing exactly?
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 -> cF.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 + 1timesTwo : Int -> Int
timesTwo n = n * 2CountingBox 0 5
|> CountingBox.map (addOne >> timesTwo)
== CountingBox 1 12 -- we mapped once, changing the value onceCountingBox 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 1CountingBox 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…