Esse post é baseado no livro Teoria das Categorias para Programadores, de Bartosz Milewski.
O Lema de Yoneda diz que:
“Uma transformação natural entre um hom-functor e qualquer outro Functor \(F\) é completamente determinado ao especificar o valor do componente inicial do hom-functor."
Para entender esse lema, vejamos duas transformações naturais:
alphaX :: Reader a x -> F x
alphaY :: Reader a y -> F y
Com tais transformações podemos desenhar o seguinte diagrama, que deve ser comutativo:
Como esse quadrado é comutativo, temos que
alphaY . Reader a f = (F f) . alphaX
. Como um Functor em uma função é
a função de alta ordem fmap
, temos que:
alphaY (fmap f) = fmap f . alphaX
, que age sobre uma função h
, ou
seja, alphaY (fmap f h) = fmap f . alphaX h
.
Como o fmap
no Functor Reader a
é apenas uma composição de funções,
temos então que alphaY (f . h) = fmap f . alphaX h
. Se fizermos
h :: a -> x
, temos que alphaY (f . h)
é uma transformação natural
entre um Reader a a
e um F a
. Para o caso particular em que x = a
e h = id
, temos então alphaY f = (F f) (alphaA id)
. Com isso temos
que alphaY f = fmap f (F a)
, ou seja, temos a definição para nosso
alpha
:
alpha :: (a -> x) -> F x
alpha h = fmap h fa
Para algum F a
. Substituindo h = id
, temos:
alpha :: F a
alpha id = fmap id fa = fa
Ou seja, a quantidade de definições de alpha
é a mesma que a
quantidade de F a
, sendo então isomórficas. Podemos prontamente
transformar um (a -> x) -> F x
em F a
fazendo alpha id
e um F a
em (a -> x) -> F x
fazendo fmap h fa
, além disso
alpha id . (fmap h) = id
e (fmap h) . (alpha id) = id
.
Se pensarmos no Functor identidade, temos que F = Identity
e:
(a -> x) -> x = a
Que pode ser lida como, dada uma função de alta ordem que recebe uma
função a -> x
e retorna um valor de x
, é isomórfica ao tipo a
.
Também temos a definição de Co-Yoneda, o complemento do Yoneda, em que:
(x -> a) -> F x = F a
Vamos utilizar o lema de Yoneda para criar uma transformação natural
entre Reader a
e Reader b
, ou seja,
alpha :: Reader a x -> Reader b x
. Pelo lema de Yoneda temos:
(a -> x) -> Reader b x = Reader b a
Reader a x -> Reader b x = Reader b a
(a -> x) -> (b -> x) = (b -> a)
Que nos remete a ideia de Functors contravariantes! Com isso podemos criar duas funções, uma delas que resolve o lado esquerdo da equação:
fromY :: (a -> x) -> b -> x
fromY f b = f (toY b)
Sendo a função toY :: b -> a
uma função que converte um tipo b
em um
tipo a
. Podemos recuperar essa função com toY = fromY id
.
A composição de =fmap=s no Haskell nem sempre é otimizada pelo compilador. Por exemplo, se tivermos o seguinte programa:
data Tree a = Bin a (Tree a) (Tree a) | Nil deriving (Eq, Show)
instance Functor Tree where
fmap f Nil = Nil
fmap f (Bin x l r) = Bin (f x) (fmap f l) (fmap f r)
instance Foldable Tree where
foldMap _ Nil = mempty
foldMap f (Bin x l r) = f x <> foldMap f l <> foldMap f r
sumTree :: Num a => Tree a -> a
sumTree = getSum . foldMap Sum
t :: Tree Integer
t = go 1
where go r = Bin r (go (2*r)) (go (2*r + 1))
takeDepth :: Int -> Tree a -> Tree a
takeDepth _ Nil = Nil
takeDepth 0 _ = Nil
takeDepth d (Bin x l r) = Bin x (takeDepth (d-1) l) (takeDepth (d-1) r)
transform :: (Functor f, Num a) => f a -> f a
transform = fmap (^2) . fmap (+1) . fmap (*2)
printTree k = print . sumTree . takeDepth k
ao executar printTree k $ transform t
, primeiro toda a árvore será
percorrida para aplicar o mapa (*2)
, em seguida toda a árvore é
percorrida novamente para aplicar (+1)
e mais uma vez para aplicar
(^2)
. Sabemos que, pelas leis do Functor temos que
fmap f . fmap g . fmap h = fmap (f.g.h)
. Podemos automatizar esse
processo utilizando o Yoneda Embedding.
Vamos criar o tipo Co-Yoneda CY
que representa o lado esquerdo do lema
de Yoneda:
data CY f a = forall b . CY (b -> a) (f b)
Precisamos definir uma instância de Functor para esse tipo:
instance Functor (CY f) where
fmap f (CY b2a fb) = CY (f . b2a) fb
E, utilizando o que aprendemos sobre Yoneda, podemos definir as funções
toCY
e fromCY
:
toCY :: f a -> CY f a
toCY = CY id
fromCY :: Functor f => CY f a -> f a
fromCY (CY f fa) = fmap f fa
Finalmente, precisamos de uma função que leva uma função f
em um
contexto de Co-Yoneda e, ao aplicá-la, remove desse contexto:
withCoyo :: Functor f => (CY f a -> CY f b) -> f a -> f b
withCoyo f = fromCY . f . toCY
Agora, se fizermos printTree k $ withCoyo transform t
, teremos:
withCoyo transform t = (fromCY . transform . toCY) t
= (fromCY . transform) (CY id t)
= fromCY $ (fmap (^2) . fmap (+1) . fmap (*2)) (CY id t)
= fromCY $ (fmap (^2) . fmap (+1)) (CY ((*2) . id) t)
= fromCY $ (fmap (^2)) (CY ((+1) . (*2) . id) t)
= fromCY (CY ((^2) . (+1) . (*2) . id) t)
= fmap ((^2) . (+1) . (*2) . id) t
Isso é a base da biblioteca Data.List.Stream
do Haskell que permite
otimizar o uso de funções fmap, filter, fold
, dentre outros.
https://bartoszmilewski.com/2014/10/28/category-theory-for-programmers-the-preface/ http://hackage.haskell.org/package/stream-fusion-0.1.2.5/docs/Data-List-Stream.html https://alpmestan.com/posts/2017-08-17-coyoneda-fmap-fusion.html