Esse post é baseado no livro Teoria das Categorias para Programadores, de Bartosz Milewski.
Podemos agora tentar encontrar a definição de um Produto utilizando os conceitos de Functors e Transformação Natural. Para isso vamos imaginar uma categoria \(C\) com diversos objetos e, então, selecionamos dois objetos \(a, b\). Esses objetos serão utilizados com a base de nosso produto.
Agora, criamos uma nova categoria, denominada \(\mathbf{2}\), que contém apenas \(2\) objetos, denominados \(1, 2\), respectivamente. Junto dessas duas categorias, podemos criar um Functor \(D : 2 \rightarrow C\) que mapeia cada objeto da categoria \(\mathbf{2}\) para os objetos \(a, b\) da categoria \(C\):
Para o próximo passo, selecionamos um objeto \(c \in C\) que representará um candidato para o tipo produto. Tal objeto também tem uma ligação com a categoria \(\mathbf{2}\) através do Functor constante \(\Delta_c\), que mapeia qualquer objeto no objeto \(c\):
Como sabemos que existem apenas dois objetos na categoria \(\mathbf{2}\), é fácil definir transformações naturais entre \(\Delta_c\) e \(D\), podemos definir a transformação \(\alpha, \beta\), em pseudo-Haskell como:
alpha :: Const c x -> D x
alpha (Const c) = D Um -- = a
beta :: Const c x -> D x
beta (Const c) = D Dois -- = b
Podemos nomear essas transformações como \(p, q\), e temos:
Por conta das restrições impostas em uma transformação natural, temos que:
p . deltaC = D Um = a
q . deltaC = D Dois = b
E temos a mesma construção que fizemos originalmente para o produto. Esse tipo de construção é denominada Cone, pois generalizando o padrão formamos uma figura similar a um cone:
A condição necessária para que essa construção seja um Cone é a de que todos os triângulos sejam comutativos.
Definindo \(c\) como o ápice de nosso Cone, queremos encontrar o melhor candidato para o ápice, dentre muitos candidatos. Utilizamos como rankeamento a fatoração \(m\) tal que dado \(c, c’\), \(c\) é melhor que \(c’\) caso exista um único \(m\) que \(p’ = p \circ m\) e \(q’ = q \circ m\). Chamamos o melhor ápice de limite do Functor D, ou \(Lim D\).
A categoria que utilizamos como base da construção é denominada Imagem (\(I\)) e geralmente é deixada implícita nos diagramas.
Se nossa categoria \(I\) for construída como uma categoria vazia, tudo que resta em nosso Cone é o conjunto de ápices, sendo um deles nosso \(Lim D\):
Nosso limite é definido por um morfismo m :: c -> d
que funciona para
todos os candidatos c
. O único tipo que pode substituir d
é o unit
()
com a função unit _ = ()
.
Um outro Cone interessante, parte da categoria \(I\) com \(2\) objetos,
mas tal que existam dois morfismos f, g :: a -> b
:
Esse padrão é conhecido como Equalizador (equalizer) e, por conta da comutatividade, temos que:
q = f. p
q = g .p
f . p = g . p
E podemos interpretar como um sistema de equações:
f :: (Double, Double) -> Double
f x y = 2*y + x
g :: (Double, Double) -> Double
g x y = y - x
p :: Double -> (Double, Double)
p t = (t, -2*t)
O conceito de Equalizer está sendo utilizado na criação dos chamados Refinement Types que permite criar assinaturas de funções com restrições nos tipos:
type Nat = {v:Int | 0 <= v}
fat :: Nat -> Nat
fat 0 = 1
fat 1 = 1
fat n = n * fat (n-1)
Essa construção garante verificar em tempo de compilação se o programa
contém alguma aplicação da função fat
para elementos negativos do tipo
Int
, reportando o erro para o usuário. Não é possível fazer esse tipo
de construção por padrão no Haskell, porém existe um projeto chamado
LiquidHaskell que permite tais construções.
A construção acima do tipo Nat
pode ser definida pelos equalizadores
f, g
:
f :: Int -> Int
f = id
g :: Int -> Int
g = abs
e formado por um tipo [Int]
cujo morfismo p
é definido como:
p :: [Int] -> Int
p = length
Como a imagem da função p
é sempre um número natural, sabemos que
nesse domínio f = g
. Veremos mais adiante que o tipo lista permite
modelar os números naturais através de um Monoid.
Um outro Cone interessante é conhecido como Pullback e consiste em três objetos \(a, b, c\) que se conectam em um padrão de ligação \(a \rightarrow b \leftarrow c\) que comutam através de um objeto \(Lim D\) da seguinte forma:
Ou seja, temos a igualdade f . p = g . q
. Esse padrão define o Tipo
Interseção em que os valores são provenientes de dois tipos com um
campo em comum. Uma outra aplicação desse padrão é na construção de um
SQL JOIN:
data Order = Order { orderId :: Int
, customerId :: Int
} deriving Show
data Customer = Customer { customerId' :: Int
, country :: String
} deriving Show
orders :: [Order]
orders = [Order 1 1, Order 2 1, Order 3 2]
customers :: [Customer]
customers = [Customer 1 "BR", Customer 2 "US"]
f :: Order -> Int
f = customerId
g :: Customer -> Int
g = customerId'
p = fst
q = snd
db :: [(Order, Customer)]
db = [(o,c) | o <- orders, c <- customers]
pullback s = (f . p) s == (g . q) s
joined = filter pullback db
main = do
putStr $ unlines $ fmap show joined
Uma outra aplicação é na forma como um compilador lida com herança de classes no paradigma de Orientação a Objetos. A categoria das classes pode ser definida como os objetos sendo as classes e o morfismo \(a \rightarrow b\) implicando que \(a\) herda os métodos de \(b\). Desenhando nosso Cone de cabeça para baixo, temos:
Assumindo que o nó \(D\) é o \(Lim D\) do nosso cone, isso implica que ele contém exatamente os métodos de \(B\) e \(C\), porém sem a duplicação dos métodos de \(A\), ou seja, é a união de \(B\) e \(C\) sem identificação de qual conjunto cada elemento pertence. O objeto \(E\) é uma classe que contém os mesmos métodos de \(D\) e mais alguns novos acrescidos em sua definição.
Como em todas as construções em Teoria das Categorias, podemos definir a construção complementar ao inverter a direção das setas dos morfismos. Um Colimite é o ápice de um Cocone, ou seja, um Cone com as direções invertidas. Por exemplo, para definir o Coproduto temos:
Analogamente podemos definir o objeto inicial como o Cocone partindo de uma imagem vazia:
O complemento do Pullback é conhecido como Pushout e define uma União Disjunta:
Esse tipo de padrão é utilizado no estudo de formação de conceitos durante o aprendizado infantil. A ideia é a de que a criança exposta a figuras rotuladas de forma abstrata infere a classificação de figuras da mesma classe. Por exemplo, imagine que a criança escuta sua mãe chamar um chihuahua e um labrador de cachorro, ao visualizar um pastor alemão ela infere que também é um cachorro dada as similaridades:
Pensando em uma Rede Neural Artificial, podemos pensar nos morfismos
atributos_chihuahua
e atributos_labrador
como duas Redes
Convolutivas que extraem as informações de imagens que caracterizam as
duas raças conhecidas de cachorros. Da mesma forma, temos os morfismos
prob_cachorro_chihuahua
e prob_cachorro_labrador
como o cálculo das
probabilidades de ser um cachorro dadas as carecterísticas extraídas das
duas raças.
Isso é um dos padrões estudados para possibilitar a separação do treinamento de redes neurais para diferentes conceitos e, após o aprendizado, fazer a composição das funções de conceito.