Functors and Map

“Functor”, alright FP, is that actually a word?

Despite not included in the built-in dictionaries of some text editors, “functor” is not “function” misspelled, and is indeed a word. 😉 (VS Code, you should know about programming terms!)

According to the Wikipedia disambiguation page, there is actually no universally accepted definition of functor, even in functional programming communities. Here we will use this term to refer to a data structure which can be applied with a map function. I’ll explain what this means!

Introducing the Map Function

If you are playing with FP for a while, maybe you are somewhat familiar with the magical map function already. In most cases, you might be using the map function to transform an array or list - it’s kind of like foreach, but focuses on the mapping between source list and result list, instead of the only performing the action defined in the foreach loop. Here is an example in Python 3.5+.

cool = [1, 3, 5, 7, 9]
# create a new list where every element in cool is incremented by 1
even_cooler = [*map(lambda x: x + 1, cool)]
# A pre Python 3.5 equivalent would be:
# even_cooler = list(map(lambda x: x + 1, cool))

print(even_cooler) # [2, 4, 6, 8, 10]

In the example above, we’ve essentially created a mapping between the source list cool and a result list even_cooler where every element in cool is incremented by 1. Due to the lazy execution nature of the map function in Python, the return value of is a “map object”, so we have to flatten it out and pack it back into a list, hence the [*map()] syntax in line 3. The mapping relationship itself is expressed via a lambda expression, which is passed into map as the first argument, whereas the list cool is the data structure that we want to apply the map function to. This is how the map function is generally used.

OK, so, the map function really does seem to be just an enhanced version of foreach! According to our definition in the beginning, the mysterious functor is just the simple list cool. We’ve learnt functor and map in just a short example! 😉 But if functor is just a made up name for a simple thing by some smart dudes, is there really anything more about the fuzz and buzz around functors?

The Art of Wrapping Things

I love using the word “wrap”. Not only because of these yummy goodies, but also that we tend to wrap a lot of things around in programming too.

Sometimes in OOP, the notion of wrapping an invisible cloak over delicate things (private fields) is called encapsulation. I also like to imagine inheritance as wrapping a parent object with extra features of the child. And as it turns out, wrapping things around in FP is very important as well. In fact, the map and functor concept is just about wrapping and unwrapping values or data in FP.

Data is the essential part of any kind of programming, but data in its purest form represent rather abstract things, so we wrap them up inside all kinds of data structures to make generalization easier - we define arrays, dictionaries, objects, even “int” and “char”. (OK, maybe it’s not quite accurate to call “int” or structs data structure, but they are just man made concept layers that represent abstract ideas.) The wrapper can look like anything, be it a kind of container you designed yourself, or a good ‘o array. We are doing this kind of wrapping daily - this layer of wrap is what makes up different “classes” or types of “things” which we can generalize, and perform operations on (think type or 🦆 typing).

For example, we might feel like to design a new kind of “4-bit unsigned retro integer” with booleans and lists like below. The numbers 1 is the value that is wrapped inside our data structure, and the dictionary itself is the “thing” that we pass around and perform operations upon.

retro_1 = {'4': False, '3': False, '2': False, '1': True} # 0001

We can define operations that are only valid on our retro 4-bit data structure/objects. Let’s create a little function that XOR’s whatever retro numbers we feed into it with 8.

def retro_xor_8(x):
  retro_8 = {'4': True, '3': False, '2': False, '1': False} # 1000
  answer = {}
  for key in x:
    answer[key] = x[key] ^ retro_8[key]
  return answer

retro_1_xor_8 = retro_xor_8(retro_1)
print(retro_1_xor_8) # {'4': False, '3': False, '2': False, '1': False} => 0000

What a useless example LOL! But you know what? What we have done here inside retro_xor_8() is something remarkable - we unwrapped the retro_1 object passed in (via directly accessing to its inside values here), performed the XOR operation bitwise, then wrapped the result back into a “4-bit unsigned retro integer” (via inserting bit by bit into an empty dictionary). The value here is the collection of the four boolean values with keys - the wrapper here is the layer of {} denoted dictionary “skin”.

Maybe you have already noticed, this is something very similar to a map operation, and the “retro integers” are almost like functors.

In fact, “map” is not just a glorified foreach. What the map function does in a more general perspective is that it always unwraps a given container, performs operations on the values inside, then wraps the result back up with a same kind of wrapper layer. It doesn’t necessarily iterate things, it just performs an unwrap-action-wrap procedure. An array or list can be applied with a map function, because they are just wrappers wrapping around a bunch of values.

Monad Is Not Far Away!

Coming up we will be exploring the relationship between functor and monads. As mentioned previously, monad is yet another specific way of wrapping things around. See you around!