Esse post é baseado no livro Teoria das Categorias para Programadores, de Bartosz Milewski.
Considere a expressão algébrica \(x^2 + 2x + 1\) que, computacionalmente, podemos representar como uma árvore de expressão:
Denotando a variável \(x\) como um objeto a de uma categoria, temos
que nossa árvore de expressão é um objeto m a, sendo m um
endofunctor contendo valores do tipo a. Uma operação comum em
expressões algébricas é a substituição de variáveis, seja para
simplificação, avaliação ou transformação. Podemos substituir toda
ocorrência de \(x\) da árvore acima por \(y - 1\), gerando a expressão
\((y-1)^2 + 2(y-1) + 1\) e a árvore:
Se \(y\) é um objeto b de nossa categoria, temos que a expressão
\(y - 1\) é um tipo m b. Nossa transformação pode ser representada
por:
,#+begin_src haskell subst :: m a -> (a -> m b) -> m b #+end_src
que é o operador bind que definimos para Monads. Portanto, uma outra forma de pensar em Monads é na manipulação de expressões algébricas via substituição.
Na matemática um Monad é representado pela letra \(T\), que representa um endofunctor, a operação join é representada pela transformação natural \(\mu\) e o return por \(\eta\):
\(\mu :: T^2 \rightarrow T\)
\(\eta :: I \rightarrow T\)
Que, aplicando um objeto a são equivalentes a:
\(\mu_a :: T (T a) \rightarrow T a\)
\(\eta_a :: a \rightarrow T a\)
Com isso, se tivermos os morfismos \(f :: a \rightarrow T b\) e \(g :: b \rightarrow T c\), a composição e definida por:
\(g \circ_T f = \mu_c \circ (T g) \circ f\)
que em Haskell se traduziria (lembrando que um Functor funciona em morfismos) em:
join (fmap g (f a))
Em álgebra, isso representa a composição de duas substituições.
Dissecando um pouco mais as definições de \(\mu, \eta\), percebemos que \(\mu\) age como a multiplicação de dois endofunctors enquanto \(\eta\) funciona como uma identidade na composição de Functors:
mu :: (m, m) -> m
=
mu :: m -> m -> m
eta :: () -> m
=
eta :: m
Essas duas definições são isomórficas com as definições de mappend e
mempty da classe Monoid. Vamos verificar se as propriedades de um
Monoid são garantidas:
mu (x, mu (y, z)) = mu (mu (x, y), z)
Para um tipo produto temos a definição de Bifunctors para mapear as funções, com isso temos:
(mu . bimap id mu) (x, (y,z)) = (mu . bimap mu id) ((x,y),z)
Para transformar essa equação em point-free, temos que aplicar uma
transformação natural alpha para rearranjar os tipos produtos:
alpha :: (a, (b, c)) -> ((a, b), c)
alpha (x, (y,z)) = ((x,y), z)
com isso temos:
(mu . bimap id mu) . alpha = (mu . bimap mu id)
A propriedade de identidade é fácil de verificar:
mu (eta (), x) = x
mu (x, eta ()) = x
que se tornam:
(mu . bimap eta id) ((), x) = lambda ((), x)
(mu . bimap id eta) (x, ()) = rho (x, ())
Podemos então generalizar uma Categoria Monoidal como toda categoria que possui um Bifunctor, chamado de produto tensorial \(otimes\) e um objeto terminal \(i\), denominado unit que satisfaçam as propriedades dos Monoids.
Com essas definições podemos chegar na definição de Monads em que temos o endofunctor \(T\), o Functor identidade \(I\), a composição de Functors \(\circ\) e duas transformações naturais:
mu :: T . T -> T
eta :: I -> T
Ou seja, um Monad é um Monoid na categoria dos endofunctors (Saunders Mac Lane).
Relembrando a definição de dois Functors adjuntos, \(L, R\), como um par
de Functors em que L :: D -> C e R :: C -> D de tal forma que a
composição deles forma o Functor identidade. Eles são equipados com dois
morfismos:
unit :: I -> R . L
counit :: L . R -> I
Podemos verificar que a composição R . L forma um Monad, sendo a
definição de unit equivalente ao return. O join pode ser
construido partindo da definição de counit:
join :: R . L . R . L -> R . L
=
join :: R . (L . R) . L -> R . L
join = R . counit . L
Como Functos adjuntos geralmente envolvem duas categorias distintas, é
difícil encontrar exemplos em Haskell. Repetindo o exemplo dado nesse
tópico, podemos fazer com que L = (,s) = Writer s e
R = (s->) = Reader s. A composição R . L dos dois define
Reader s (Writer s a) que é a definição do Monad State.
Para esse Monad, as definições de unit :: a -> Reader s (Writer s a) e
counit :: Writer s (Reader s a) -> a podem ser dadas por:
unit :: a -> s -> (a, s)
unit x = \s -> (x, s)
counit :: (s -> a, s) -> a
counit (f, s) = f s
Reparem que a definição de unit é equivalente ao return de nosso
Monad e counit equivale a runState seguido de uncurry. Como
definimos o join?
join :: State s (State s a) -> State s a
-- = R . counit . L
join ssa = State (uncurry runState . runState ssa)
Da mesma forma podemos pensar em Comonads como uma categoria de endofunctors, porém o dual do Monoid, o Comonoid. Nessa categoria temos dois morfismos:
eps :: T -> I -- extract / counit
delta :: T -> (T, T) -- duplicate
Analogamente, percebemos que eps representa o counit, com T = L.R e,
utilizando eta :: I -> R.L podemos fazer
delta = L . eta . R = L . R . L . R = (T, T). Com isso podemos definir
um Comonoid como:
class Comonoid m where
split :: m -> (m, m)
destroy :: m -> ()
destroy _ = ()
Dessa forma, um Comonad é um Comonoid da categoria dos endofunctors..
https://bartoszmilewski.com/2014/10/28/category-theory-for-programmers-the-preface/