Hello. Let’s make a box for putting functions in…
newtype FuncBox b c
= FuncBox { runFuncBox :: b -> c }
…and a function…
length :: String -> Int
length s = foldl' (\c _ -> c+1) 0 s
…and then let’s put a function in this box:
length' :: FuncBox String Int
= FuncBox length length'
Great. A function in a box. You may be concerned that something interesting is going on here, so just to assure you that it’s not, let’s run the function using runFuncBox
, to reassure ourselves that this box is doing no funny business and we’re really just unwrapping that newtype
.
length'' :: Int
= runFuncBox length' "dog"
length'' -- length'' == 3
OK. Good stuff. We now have a very longwinded way of running the length
function. Good stuff. Big day.
The plot thickens
Now what if want to run this weird function, but instead of having a String
to hand, we only have an Animal
…
data Animal = Horse | Dog | Cat
deriving (Show)
I can see you there. You’re getting excited and jumping straight for our old pal Contravariant aren’t you. But wait! That’s not an entirely terrible decision but it turns out there are other problems that we must also solve.
Oh no
That’s right. Heartbreaking, it also turns out that our somewhat contrived API can output numbers, but instead only lists full of a delicous datatype called Egg
.
data Egg = Egg
We can turn any Int
into an [Egg]
using this excellent function.
repeatEgg :: Int -> [Egg]
repeatEgg s= replicate s Egg
threeEggs :: List Egg
= repeatEgg 3
threeEggs -- threeEggs == [Egg, Egg, Egg]
OK. So somehow we need to turn an Animal
into a List
of Egg
. We could contramap
over our input to turn Animal
into String
, run the original length
function, and then fmap
over the result to turn Int
into [Egg]
. That could work. But what if we could solve this problem using a typeclass, that would be pretty fucking snazzy wouldn’t it?
Hold on, he’s only gone and done it
Looks like a job for our new friend Profunctor
I reckon. Let’s have a wee look in ghci
and see what what the hell it’s deal is.
import Data.Profunctor
Data.Profunctor> :i Profunctor
class Profunctor (p :: _ -> _ -> \*) where
dimap :: (a -> b) -> (c -> d) -> p b c -> p a d
lmap :: (a -> b) -> p b c -> p a c
rmap :: (b -> c) -> p a b -> p a c
{-# MINIMAL dimap | lmap, rmap #-}
Look at that! It gives a Contravariant
with the lmap
function, and a regular Functor
with rmap
, or everything smashed together in this new exciting dimap
function. Let’s look at that a little closer.
dimap :: (a -> b) -> (c -> d) -> p b c -> p a d
That’s pretty beastly, let’s put in some real concrete things.
dimap :: (Animal -> String) -- function to go at the start
-> (Int -> [Egg]) -- function to go at the end
-> FuncBox String Int -- original container
-> FuncBox Animal [Egg] -- exciting new container
OK. Let’s implement it for our FuncBox
.
Instances, binstances, dinstances
import Data.Profunctor
instance Profunctor FuncBox where
FuncBox f)
dimap before after (= FuncBox (after . f . before)
That’s all really. The .
is function composition, so therefore our function unwraps the original function and calls it f
, then returns a new FuncBox
which runs the before
function (ie, the a -> b
one), then the original f
function, and finally the after
function (c -> d
). The resulting FuncBox
can be used exactly as before, and nobody using it knows how secretly clever it is.
Let’s use it to make our all important function for turning an Animal
in to a [Egg]
, using our length'
and repeatEgg
functions from earlier.
dimapped :: FuncBox Animal [Egg]
dimapped= dimap show repeatEgg length'
This gives us a new FuncBox
that turns Animal
into [Egg]
, but of course we all know that underneath the hood this function it’s converting Animal -> String -> Int -> [Egg]
. Let’s see it in action:
test :: [Egg]
= runFuncBox dimapped Dog
test -- test == [Egg, Egg, Egg]
test2 :: Int
= runFuncBox dimapped Horse
test2 -- test2 == [Egg, Egg, Egg, Egg, Egg]
Brilliant. What an absolutely useful non-waste-of-time. OK. So these are stupid examples, but hopefully they give you a rough idea what a Profunctor
is under the hood. You often hear of their use in Lenses, as using dimap
on a function for changing two small things can make it into a function that changes a small thing inside a much bigger thing, and they compose in the same nice way.
Good stuff.
Feel free to shout your brains about how stupid and wrong I am via the usual channels.
Further reading: