Functors

Published:

Functors seem to have many definitions [1], according to [2] anything that can be mapped over while satisfying certain laws are functors. It might make sense to think functor as something that encapsulates aa and when "mapped" it outputs something of encapsulated type bb, a kind of data conversion. The encapsulated type bb must also be a functor [2].

An Functor Implementation in Python

In this example implementation, we have added mapping for iterables and support for multiple functions as parameters. It can also be used as single function parameter.

class FunctorCategory:
    """
    An iterable to be passed to Functor.
    Must be separately defined because the Functor cannot know
    whether the mapping is applied to an iterable or items in the iterables.
    """
    def __init__(self, data: typing:Iterable):
        self.iterable: typing.Iterable = data


class Functor:
    def __init__(self, data: any):
        self.data: any = data

    def fmap(self, *functions) -> Functor:
        """
        Maps the data to something else.
        This satifies the required laws:
        - Identity
            if f(x) = x then x.map(f) = x
        - Composition
            if (f * g)(x) = f(g(x)) = y then x.map(g).map(f) = y
        """
        if type(self.data) == FunctorCategory:
            mapped_values = map(lambda item: self._apply_functions(item, *functions), self.data.iterable)
            result = FunctorCategory(list(mapped_values))
        else:
            result = self._apply_functions(self.data, *functions)
        return Functor(result)

    def _apply_functions(self, data, *functions):
        mapped_value = data
        for function in functions:
            data = function(function)
        return mapped_value

The reason for the result being wrapped is that the result has to be chainable [2].

class Restaurant:
    def __init__(self, address):
        self.address = address

    @staticmethod
    def to_company(data: Restaurant):
        return Company(address=data.address)


restaurant = Restaurant("Lunchy Street 2")
Functor(restaurant).fmap(Restaurant.to_company).fmap(Company.to_share_value)

In the example we can map restaurant to a company object, which in turn returns the share value of the company.

When to use?

When it is anticipated that multiple types of data (e.g. objects) are to be converted to same datatype using similar function. For example Bank, Restaurant, and Museum can be converted to Company using a same function. This way we don't need to implement to_company function for every datatype.

Another reason to use functors is that any function can be used to map data as long as the objects satisfy the requirements of the function. In object-oriented paradigm the same could be achieved using inheritance.

# Object-oriented equivalent
class Restaurant(Company):
    def __init__(self, address):
        self.address = address

    # to_company is inherited


restaurant = Restaurant("Lunchy Street 2")
Restaurant.to_company().to_share_value()

In the example, OOP obviously beats the functor pattern but when the same has to be applied to multiple objects such as

entities = [Bank(), Restaurant()]
category = FunctorCategory(entities)
Functor(category).map(to_company, to_share_value)

and its OOP equivalent

def fmap(entities: [], function_names: []):
    share_values = []

    for obj in entities:
        mapped_value = obj
        for function_name in function_names:
            function = getattr(obj, function_name)
            mapped_value = function(obj)
        share_values.append(mapped_value)
    return list(share_values)

entities = [Bank(), Restaurant()]
fmap(entities, 'to_company', 'to_share_value')

or in OOP we can use map as many times as the number of functions or hardcode the functions. Either way functors are more compact and generic.

References

  1. https://stackoverflow.com/questions/2030863/in-functional-programming-what-is-a-functor
  2. https://medium.com/@dtinth/what-is-a-functor-dcf510b098b6
  3. https://medium.com/@dtinth/what-s-not-a-functor-57a081131555