Esse post é baseado no livro Teoria das Categorias para Programadores, de Bartosz Milewski.
Em Teoria das Categorias Functor é um mapa entre categorias que mapeia objetos e morfismos de uma categoria \(C\) para uma categoria \(D\), ou seja, \(F : C \rightarrow D\) é um functor da categoria \(C\) para a categoria \(D\) se dados objetos e morfismos \(a, b, f \in C\), sendo \(f : a \rightarrow b\) então \(F a, F b, F f \in D\) são objetos e morfismos da cateogria destino e \(F f : F a \rightarrow F b\) é um morfismo dessa categoria. Graficamente temos:
Esse mapa não apenas transforma uma categoria em outra, mas também preserva sua estrutura, ou seja, tanto os morfismos identidades como as composições são mantidas intactas:
\(F id_{a} = id_{F a}\)
e
\(h = g . f \implies F h = F g . F f\)
Um Functor na categoria \(\mathbf{Hask}\) na verdade é um mapa \(F : \mathbf{Hask} \rightarrow \mathbf{Hask}\), ou seja, um mapa da categoria para ela mesma. Isso também é conhecido como endofunctor.
Na definição acima, podemos perceber que um functor \(F\) pode ser visto como um tipo paramétrico, pois sempre está acompanhado de um tipo \(a\). Dizemos que o tipo \(F\) contém o tipo \(a\), ou seja, ele cumpre o papel de um container.
A ideia de container é familiar em programação, aprendemos tal conceito em estrutura de dados quando somos introduzidos a listas, árvores, mapas. Essas estruturas são paramétricas, não temos simplesmente uma lista, mas sim uma lista de inteiros, uma árvore de strings, um mapa de inteiro para string.
Vamos revisar a definição de uma lista em Haskell:
data List a = Empty | a : (List a)
Uma lista do tipo a
pode não conter nada (Empty
) ou conter um
elemento do tipo a
seguido por outra lista (encadeada) do mesmo tipo.
Na nossa definição de Functors, \(F = List\). Porém, um Functor é, na
verdade, mais do que um container, ele também é um mapa de morfismos. A
definição de um Functor em Haskell evidencia essa propriedade:
class Functor F where
fmap :: (a -> b) -> F a -> F b
que pode ser lida como “Para um tipo \(F\) ser um Functor você deve me
fornecer uma função fmap
para ele que recebe uma função de a
para
b
e um valor do tipo F a
e me devolve um valor do tipo F b
.”
Por exemplo, se \(F\) for uma lista, a
o tipo Int
e b
o tipo
Char
, se você me entregar uma função f :: Int -> Char
e uma lista de
Int
, eu te devolvo uma lista de Char
.
Notem porém que em Haskell uma função sempre tem apenas um argumento, a
função fmap
deve, na verdade, ser lida como:
fmap :: (a -> b) -> (F a -> F b)
ou seja, a função fmap
recebe uma função f :: a -> b
e me retorna
uma função g :: F a -> F b
. Isso é conhecido como lift de uma função
em programação funcional. No nosso exemplo, me entregue uma função de
f :: Int -> Char
e te devolvo uma função g :: [Int] -> [Char]
. Um
mapa de morfismos! Apesar disso, a primeira interpretação de fmap
é
mais fácil de ser implementada em Haskell. Vejamos o exemplo para lista:
instance Functor List where
fmap f [] = []
fmap f (x:xs) = f x : fmap f xs
Nessa definição passamos como argumentos uma função f
e uma lista. Se
a lista for vazia, não há nada a ser feito, se a lista contém pelo menos
um elemento x
(usando o pattern matching x:xs
), aplicamos f
no
elemento x
e concatenamos com o resultado da chamada recursiva de
fmap
no restante da lista:
let xs = 1 : 2 : 3 : []
fmap show xs
-- fmap show (1:xs) = show 1 : fmap show xs
-- = show 1 : fmap show (2:xs)
-- = show 1 : show 2 : fmap show xs
-- = show 1 : show 2 : show 3 : fmap show []
-- = show 1 : show 2 : show 3 : []
O fmap
aplica a função f
em todos os elementos da lista,
transformando um List a
em um List b
.
Vamos definir diversos Functors existentes no Haskell e, quando pertinente, seu equivalente em outras linguagens. O Functor mais simples que existe é aquele que não armazena nada:
data Const b a = Const b
instance Functor (Const b) where
fmap _ (Const x) = Const x
Esse Functor projeta os valores do tipo a
em um valor constante do
tipo b
, a função fmap
simplesmente ignora a função e continua com o
seu valor constante. Apesar de sua definição simples, ele tem diversas
utilidades em estrutura de dados mais complexas, além de permitir a
construção de Functors de forma automática, conforme veremos mais
adiante.
Um outro exemplo de Functor é aquele que guarda um único valor de certo tipo:
data Identity a = Identity a
instance Functor Identity where
fmap f (Identity x) = Identity (f x)
O Functor Identity a
é isomórfico ao tipo a
, a função fmap
simplesmente aplicar a função f
no valor contido dentro do container.
Temos um Functor que descarta informação (Const
) e outro que armazena
um único elemento de um certo tipo, como definimos um Functor que pode
não armazenar nada ou armazenar um único elemento? Podemos construir
esse Functor com o tipo soma dos Functors Const
e Identity
:
data NadaOuUm a = Either (Const () a) (Identity a)
Esse tipo é isomórfico a um outro tipo definido anteriormente, o tipo
Maybe
(optional
em C++) que, relembrando, é definido como:
data Maybe a = Nothing | Just a
Para verificar que ambos os tipos são isomórficos basta definirmos:
f :: Maybe a -> NadaOuUm a
f Nothing = Const ()
f (Just x) = Identity x
g :: NadaOuUm a -> Maybe a
g (Left (Const ())) = Nothing
g (Right (Identity x)) = Just x
f . g = id
g . f = id
Baseado na definição de NadaOuUm
, como podemos escrever fmap
para o
tipo Maybe
?
instance Functor Maybe where
-- fmap _ (Const ()) = Const ()
fmap _ Nothing = Nothing
-- fmap f (Identity x) = Identity (f x)
fmap f (Just x) = Just (f x)
Precisamos lembrar que apenas definir a função fmap
não é suficiente,
precisamos verificar se elas obedecem as propriedades de um Functor, ou
seja, temos que verificar se:
fmap id = id
fmap (g . f) = fmap g . fmap f
Como em Haskell todas as funções são definidas por igualdade, é possível
provar a corretude ou propriedade de certa função utilizando o
raciocínio equacional. Vamos verificar se o fmap
do tipo Maybe
possui as propriedades desejadas:
fmap id Nothing = id Nothing
Nothing = Nothing
fmap id (Just x) = id (Just x)
Just (id x) = Just x
Just x = Just x
fmap (g . f) Nothing = (fmap g . fmap f) Nothing
Nothing = fmap g (fmap f Nothing)
Nothing = fmap g Nothing
Nothing = Nothing
fmap (g . f) (Just x) = (fmap g . fmap f) (Just x)
Just ((g . f) x) = fmap g (fmap f (Just x))
Just (g (f x)) = fmap g (Just (f x))
Just (g (f x)) = Just (g (f x))
Quando aprendemos sobre a categoria Kleisli, vimos o container Writer
definido como:
-- data Writer s a = (Identiy a, Const s a)
data Writer s a = (a, s)
Como ficaria a definição de fmap
para esse tipo? Agora temos o produto
do Functor Identity
com o Functor Const
:
instance Functor (Writer s) where
-- fmap f (x, s) = (fmap f x, fmap f s)
-- (fmap f (Identity x), fmap f (Const s))
-- Identity (f x), Const s
fmap f (x, s) = (f x, s)
Um último Functor que será bastante utilizado mais adiante é o Reader
,
em Haskell ele é definido como:
type Reader r a = r -> a
Ou seja, uma função de r
para a
. Assim como nos exemplos anteriores,
a função fmap
age sobre o último parâmetro do nosso tipo, então dada
uma função a -> b
, precisamos de uma função que transforme um r -> a
para um r -> b
. Isso é simplesmente a composição de funções!
instance Functor (Reader r) where
fmap = (.)
Esse Functor nos diz algo muito importante! Se funções são Functors, funções podem ser interpretadas como containers! Vimos anteriormente que funções puras podem ser memoizadas, ou seja, ter seus resultados pré-computados e armazenados em um container. O inverso também é válido, um container pode ser representado como uma função. E realmente, em Haskell um container é uma função.
O container Maybe Int
, por exemplo, é definido pelas funções Nothing
que retorna sempre um valor constante e a função Just
que recebe um
valor x
do tipo Int
. Com essa intuição, podemos definir tipos
infinitos (ex.: stream de dados):
-- lista infinita com os números naturais
nat = [1..]
nat = 1 : fmap (+1) nat
-- 1 : (+1) 1 : (+1) (+1) 1 : ...
As propriedades de um Functor são importantes para permitir a composição de dois ou mais Functors, isso nos permite definir funções para estruturas mais complexas de forma simplificada:
maybeTail :: [a] -> Maybe [a]
maybeTail [] = Nothing
maybeTail (x:xs) = Just xs
square :: Integer -> Integer
square x = x*x
is :: [Integer]
is = [1 .. 10]
fmap (fmap square) (maybeTail is)
=
(fmap . fmap) square (maybeTail is)
Em C++ precisamos definir a função fmap
com os templates do tipo a
e
do tipo b
. As definições de fmap
para o tipo optional
e para o
tipo vector
ficam:
template<class A, class B>
std::optional<B> fmap(std::function<B(A)> f, std::optional<A> opt)
{
if (!opt.has_value())
return std::optional<B>{};
else
return std::optional<B>{ f(*opt) };
}
template<class A, class B>
std::vector<B> fmap(std::function<B(A)> f, std::vector<A> v)
{
std::vector<B> w;
std::transform(std::begin(v), std::end(v), std::back_inserter(w) , f);
return w;
}
e como exemplo de uso:
int dobra(int x) {
return 2*x;
}
int main()
{
std::optional<int> o1, o2;
std::function<int(int)> f = dobra;
std::vector<int> v{ 1, 2, 3, 4 };
o1 = {3};
o2 = {};
std::cout << fmap(f, o1).value_or(-1) << std::endl;
std::cout << fmap(f, o2).value_or(-1) << std::endl;
for (auto const& c : fmap(f, v))
std::cout << c << ' ';
std::cout << std::endl;
return 0;
}
Em Python, podemos utilizar o singledispatch
, porém temos que inverter
a ordem dos parâmetros uma vez que o tipo paramétrico é sempre o
primeiro argumento:
from functools import singledispatch
class Maybe:
def __init__(self, x = None):
self.val = x
@singledispatch
def fmap(a, f):
print("Not implemented for" + str(type(a)))
@fmap.register(list)
def _(l, f):
return list(map(f, l))
@fmap.register(Maybe)
def _(m, f):
if m.val is None:
m.val = None
else:
m.val = f(m.val)
return m
f = lambda x: x*2
l = [1,2,3]
m1 = Maybe(2)
m2 = Maybe()
print(fmap(l, f))
print(fmap(m1, f).val)
print(fmap(m2, f).val)
https://bartoszmilewski.com/2014/10/28/category-theory-for-programmers-the-preface/