Functors
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 and when "mapped" it outputs something of encapsulated type , a kind of data conversion. The encapsulated type 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.