A programação funcional, especialmente na linguagem Haskell, repousa sobre fundamentos matemáticos que, embora abstratos, fornecem uma base precisa para a compreensão de como as máquinas processam informações. Entre estes fundamentos, os conceitos da Teoria das Categorias ocupam uma posição central.
Neste texto, a curiosa leitora explorará como ideias puramente matemáticas, objetos, morfismos, composição, functores, transformações naturais e monads, podem ser traduzidos em artefatos de código concretos e poderosos. Nosso objetivo é desmistificar esses termos e demonstrar como eles fornecem ferramentas interessantes para a engenharia de software. Começando, como não poderia deixar de ser, pela própria Teoria das Categorias.
Teoria das Categorias: a base conceitual
A Teoria das Categorias é um dos ramos da matemática que estuda composição. Ela não se importa com o que as coisas são, mas sim com como elas se relacionam e se compõem.
O que é uma categoria?
Uma categoria é uma estrutura matemática, similar a um grafo direcionado, composta por:
Objetos: . A cuidadosa leitora pode interpretá-los como tipos de dados (ex: Int, String) ou conjuntos.
Morfismos: . São as “setas” ou transformações entre objetos (ex: funções, como show :: Int -> String).
Operação de composição: . Uma operação que combina morfismos de forma associativa.
Morfismo identidade: . Para cada objeto , existe uma seta que não faz nada.
Esta estrutura pode ser vista na figura a seguir:
Para que essa estrutura seja formalmente uma categoria, duas leis são necessárias e indispensáveis:
Associatividade: Para morfismos , e :
Identidade: Para qualquer morfismo :
Essas leis garantem que as composições, caminhos, dentro da categoria se comportem de maneira previsível.
Exemplo concreto: a categoria Hask
Em Haskell, existe uma categoria implícita, e idealizada, chamada Hask:
Elemento
Em Hask
Objetos
Tipos Haskell (Int, String, etc.)
Morfismos
Funções puras e totais
Composição
Operador (.)
Identidade
Função id
A composição de funções em Haskell é feita com o operador (.), definido como:
(.) :: (b -> c) -> (a -> b) -> (a -> c)
E a função identidade é:id :: a -> a
As leis da categoria são, majoritariamente, garantidas pelo compilador:
1
2
3
4
5
6
7
-- definimos composição por .-- Associatividade pode ser verificada assim:h.(g.f)==(h.g).f-- Identidadef.id==f-- à direitaid.f==f-- à esquerda
A figura a seguir ilustra a categoria Hask:
Nota sobre a pureza de Hask: a atenta leitora deve observar que, na prática, Hask não é uma categoria matemática perfeita. Isso se deve à existência de funções parciais, que falham para certas entradas e valores indefinidos (como undefined ou error "..."), que violam a propriedade de que morfismos devem ser totais. Contudo, ela serve como uma aproximação conceitual poderosa.
Outros Exemplos de Categorias
Embora Hask seja o nosso principal objeto de estudo, a Teoria das Categorias ganha vida por meio de exemplos e aplicações, mesmo na álgebra pura.
A Categoria Set
A categoria Set é, talvez, a categoria mais intuitiva, servindo de base para muitas outras.
Elemento
Em Set
Objetos
Quaisquer conjuntos (ex: , , ).
Morfismos
Funções (totais) entre conjuntos (ex: definida por ).
Composição
A composição padrão de funções, .
Identidade
A função identidade para cada conjunto .
As leis da categoria são satisfeitas:
Associatividade: A composição de funções é inerentemente associativa, .
Identidade: A função atua como elemento neutro, e .
A Categoria Poset
Uma categoria Poset, de Partially Ordered Set, ou Conjunto Parcialmente Ordenado, é uma construção mais sutil, em que a própria relação de ordem define os morfismos.
Seja um conjunto com uma relação de ordem parcial , reflexiva, antissimétrica e transitiva. Neste caso, teremos:
Elemento
Em Poset
Objetos
Os elementos do conjunto (ex: ).
Morfismos
A própria relação. Existe um morfismo se, e somente se,.
Composição
A transitividade da relação.
Identidade
A reflexividade da relação.
Para que esta seja uma categoria, as leis devem ser satisfeitas:
Associatividade: se existe um morfismo (ou seja, ) e (ou seja, ), a composição exige um morfismo . Neste cenário, a propriedade da transitividade ( e ) garante que este morfismo existe.
Identidade: para todo objeto , deve existir um morfismo . A propriedade da reflexividade () garante que este morfismo de identidade sempre existe.
Exercício 1
Análise das Leis: Por que a lei da identidade é definida como ? Explique por que (por exemplo) não faria sentido em termos de tipos.
Morfismos: Na categoria Hask, a função read :: String -> Int é um morfismo válido? Justifique sua resposta considerando a definição de morfismo em Hask. (Dica: o que acontece se read "oi" for chamado?)
Functores: mapeando categorias
Se categorias são universos de objetos e morfismos, um Functor é um tradutor que mapeia um universo para outro preservando sua estrutura fundamental. Formalmente dizemos:
Um Functor é um mapeamento que preserva a estrutura das categorias e :
Mapeia Objetos:
Mapeia Morfismos:
Este mapeamento deve obedecer a duas leis:
Preservação da identidade:
Preservação da composição:
Functores em Haskell
Em Haskell, quase sempre lidamos com endofunctores, functores que mapeiam Hask para Hask. A typeclassFunctor captura essa ideia:
1
2
3
4
5
6
7
classFunctorfwhere\fmap::(a->b)->fa->fb-- `f` é o mapeamento de objetos (ex: `Int` -> `Maybe Int`).-- `fmap` é o mapeamento de morfismos (ex: `(+1)` -> uma função que aplica `(+1)` dentro do `Maybe`).--`fmap` aplica uma função "dentro" de um contexto ou "container" (`f`).
As leis do Functor em Haskell são a tradução direta das leis matemáticas:
1
2
3
4
5
-- Preservação da identidade\fmapid==id-- Preservação da composição\fmap(g.f)==fmapg.fmapf
O Problema do Functor e a Solução Applicative
O fmap é excelente, mas tem uma limitação: ele só funciona quando a função, morfismo, está “do lado de fora”, pura. Neste caso, precisamos lidar com o que acontece se a própria função estiver dentro do contexto?
1
2
Just(+5)::Maybe(Int->Int)\Just10::MaybeInt
Não podemos usar fmap. A assinatura de fmap é (a -> b) -> f a -> f b, mas o que temos é f (a -> b).
Isso significa que fmap espera uma função pura como primeiro argumento (a -> b), enquanto no nosso caso a função está dentro do mesmo contexto f. Em outras palavras, fmap consegue aplicar uma transformação sobre um valor encapsulado, mas não consegue aplicar uma função que também está encapsulada.
A atenta leitora deve observar que o obstáculo aqui não é apenas sintático, mas estrutural: fmap opera em um único nível de contexto, e o que temos é uma aplicação entre dois valores contextualizados, uma função em f (a -> b) e um argumento em f a.
Applicative: aplicando funções em contextos
Para resolver as limitações do fmap, surge a typeclassApplicative, que estende a typeclassFunctor:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
classFunctorf=>Applicativefwherepure::a->fa(<*>)::f(a->b)->fa->fb-- `pure`: injeta um valor puro no contexto (o "menor" contexto possível).-- `(<*>)` (lê-se "ap"): aplica uma função contextualizada a um valor contextualizado.--Exemplo (resolvendo o problema anterior):Just(+5)<*>Just10-- Resulta em: Just 15-- E se quisermos somar dois valores em contexto?pure(+)<*>Just5<*>Just10-- Resulta em: Just 15-- Ele também propaga falhas (Nothing)pure(+)<*>Just5<*>Nothing-- Resulta em: Nothing
A typeclassApplicative é excelente para combinar múltiplos valores independentes que estão dentro de um mesmo contexto. Ela permite aplicar funções de múltiplos argumentos sem precisar extrair explicitamente os valores do contexto, preservando a pureza e a composicionalidade do código.
O operador <*> realiza a aplicação sequencial de funções em contexto a valores também contextualizados, enquanto pure injeta uma função ou valor puro nesse contexto para iniciar a cadeia de aplicações.
Por exemplo, considere a monad Maybe, que veremos com mais cuidado a seguir e que representa computações que podem falhar:
1
2
pure(+)<*>Just5<*>Just10-- Resultado: Just 15
Neste caso, tanto a função (+) quanto os valores 5 e 10 são combinados dentro do contexto Maybe. Se algum deles for Nothing, o resultado de toda a expressão também será Nothing, mantendo a coerência do contexto:
1
2
pure(+)<*>Just5<*>Nothing-- Resultado: Nothing
Da mesma forma, o comportamento se estende a listas, que representam computações com múltiplos resultados possíveis:
Aqui, o Applicative executa todas as combinações possíveis de multiplicação entre os elementos das duas listas, produzindo uma lista com todos os resultados, uma espécie de produto cartesiano funcional.
Em resumo, o Applicative generaliza o Functor: enquanto fmap aplica uma função pura a um valor em contexto, o Applicative permite aplicar funções em contexto a múltiplos valores em contexto, promovendo composição estruturada e independente dentro de ambientes computacionais.
Exercício 2
Em Haskell, defina as assinaturas de tipo e implemente exemplos de uso para as funções pure, just e Maybe.
Leis do Functor: Prove que a implementação de fmap para Maybe obedece às duas leis do functor.
Leis do Functor para Listas: Prove que a implementação de fmap para listas (map) obedece à lei da composição: fmap (g . f) == fmap g . fmap f.
Uso de Applicative: Usando pure e (<*>), escreva uma expressão que combine três Maybe String em um único Maybe String (concatenando-os).
val1 = Just "a"
val2 = Just "b"
val3 = Just "c"
(Dica: pure (++) <*> ...)
Computações Dependentes e o Nascimento das Monads
A Applicative é poderosa, mas ainda limitada. Ela funciona para computações independentes. Mas e se a próxima computação depender do resultado da computação anterior? Caracterizando funções em pipeline onde o resultado de uma etapa influencia a próxima.
Não podemos usar (<*>). Precisamos do valor Userde dentro do Maybe para poder passá-lo para buscarPermissoes.
É para resolver esse encadeamento dependente que surge a Monad.
Definição Categórica
Formalmente, uma Monad em uma categoria é uma tripla que consiste em:
— um endoFunctor (ex: Maybe).
— uma transformação natural chamada unidade (pure/return). Ela pega um objeto e o “injeta” no Functor, .
— uma transformação natural chamada multiplicação (join). Ela “achata” um Functor aninhado, .
A estrutura deve obedecer a certas leis, diagramas comutativos, que garantem associatividade e identidade.
A Monad na Prática: Haskell
A definição matemática é elegante, mas a definição em Haskell é, para muitos, mais prática.
A definição moderna (pós-GHC 7.10) da typeclassMonad é:
1
2
3
classApplicativem=>Monadmwhere(>>=)::ma->(a->mb)->mb-- return = pure (não é mais parte da classe)
A atenta leitora deve prestar a atenção à dois pontos importantes:
Toda Monad é também Applicativee, portanto, Functor.
A definição mínima é apenas o operador (>>=) (lê-se bind). A função return agora é apenas um sinônimo para pure.
O operador (>>=) é a essência do encadeamento dependente:
1
(>>=)::ma->(a->mb)->mb
Ele pega (1) um valor no contexto m a, e (2) uma função (a -> m b) que sabe o que fazer com o valor a puro. O bind cuida de extrair a de m a e passá-lo para a função.
Exemplos:
1
2
3
4
5
-- Sucesso:\Just5>>=(\x->Just(x+1))-- Resulta em: Just 6-- Falha (o 'bind' faz o short-circuit):\Nothing>>=(\x->Just(x+1))-- Resulta em: Nothing (a função nem é executada)
Relação entre join e bind
As duas definições de Monad, matemática com e Haskell com (>>=) são equivalentes. Podemos definir uma em termos da outra:
bind em termos de join + fmap
Se tivéssemos join :: m (m a) -> m a, poderíamos definir bind: m >>= f = join (fmap f m) .
Análise dos tipos a cuidadosa leitora deve verificar:
m :: m af :: a -> m bfmap f m :: m (m b) (Contexto aninhado!)
join (...) :: m b (Achatado!)
join em termos de bind
Em Haskell, podemos definir join facilmente:
1
2
join::Monadm=>m(ma)->ma\joinmma=mma>>=id
Análise dos tipos:
mma :: m (m a)id :: m a -> m a (Aqui, o a em (a -> m b) é m a)
(>>=) aplica id ao conteúdo m a interno, achatando o resultado.
A Categoria de Kleisli — composição monádica como morfismo puro
Toda Monad em uma categoria define uma nova categoria, chamada Categoria de Kleisli, denotada por , onde é o endofunctor que representa a monad.
Em termos intuitivos, a categoria de Kleisli é o espaço onde as funções que retornam valores em contexto (por exemplo, a -> Maybe b ou a -> IO b) se comportam como morfismos “puros”. Isso permite raciocinar sobre computações com efeitos da mesma forma que raciocinamos sobre funções puras.
Formalmente, para uma monad em :
Objetos: são os mesmos objetos de .
Morfismos: para objetos e , temos
ou seja, as setas em são funções do tipo .
Composição: é definida usando a operação bind, ou equivalentemente e o funtor:
T B e B -> T C e sua composição Kleisli.">
Em Haskell, essa composição é implementada pelo operador (>=>):
Identidade: para cada objeto , a identidade é dada por ,
que em Haskell corresponde a return.
Intuição: composição de computações com efeitos
Na categoria original , funções puras compõem-se normalmente:
Na categoria de Kleisli, as setas são funções que retornam valores em contexto:
Como não podemos compor diretamente f e g, precisamos da estrutura monádica para “encadear” essas computações:
Assim, (>=>) é a composição categórica em .
Isso justifica matematicamente por que bind (>>=) é a operação central das Monads — ele é a composição de morfismos na categoria de Kleisli.
Exemplo em Haskell: Maybe e IO como categorias de Kleisli
Aqui, h é a composição de f e g dentro da categoria de Kleisli de Maybe.
O operador (>=>) garante que o Nothing propague corretamente e que o resultado só exista se todas as etapas anteriores forem bem-sucedidas.
Com IO
1
2
3
4
5
6
7
8
9
10
lerNumero::IOIntlerNumero=doputStrLn"Digite um número:"readLnmostrarDobro::Int->IO()mostrarDobron=putStrLn("O dobro é: "++show(2*n))programa::IO()programa=lerNumero>=>mostrarDobro$()
No exemplo acima, lerNumero e mostrarDobro são funções () -> IO a e a -> IO b.
Na categoria de Kleisli da monad IO, elas podem ser compostas diretamente, o que garante a sequencialidade pura dos efeitos.
A categoria de Kleisli formaliza o princípio de que Monads permitem compor funções com efeitos dentro de uma estrutura que respeita as leis da composição associativa e da identidade.
Ela mostra que, mesmo quando os efeitos são inevitáveis — exceções, estado, I/O —, a composição continua sendo um processo matematicamente puro e previsível.
Em resumo:
Conceito
Categoria
Categoria de Kleisli
Morfismo
Identidade
(return)
Composição
(>=>)
A Kleisli é, portanto, o ambiente natural das Monads — o espaço onde funções com efeitos podem ser tratadas como morfismos puros, e onde a teoria das categorias revela sua força como modelo formal da programação funcional.
Leis das Monads (em Haskell)
Toda instância de Monad deve obedecer a três leis, análogas às leis categoriais:
Lei
Código
Identidade à esquerda
return a >>= f == f a
Identidade à direita
m >>= return == m
Associatividade
(m >>= f) >>= g == m >>= (\x -> f x >>= g)
Essas leis garantem que o encadeamento de computações é previsível e que return é neutro.
O açúcar sintático do-notation
Encadear (>>=) pode ficar visualmente poluído. Chamamos este problema de o *inferno do callback. A linguagem Haskell fornece a do-notation como um açúcar sintático que é traduzido diretamente para chamadas de (>>=).
Este código com do:
1
2
3
4
main_do=dox<-Just5y<-Just10return(x+y)-- Resulta em: Just 15
É traduzido pelo compilador para este código com (>>=):
Modela computações que podem falhar, propagando Nothing (short-circuit) automaticamente.
1
2
3
4
5
6
7
8
9
safeDiv::Int->Int->MaybeIntsafeDiv_0=NothingsafeDivxy=Just(x`div`y)program=doa<-safeDiv102-- a = 5b<-safeDiva0-- b = Nothingc<-safeDivb1-- Esta linha nunca executareturn(c+1)-- O resultado final é Nothing
Either Monad — falhas com mensagem
Similar ao Maybe, mas carrega um valor Left String no caso de erro.
1
2
3
4
5
6
divide::Double->Double->EitherStringDoubledivide_0=Left"Divisão por zero!"dividexy=Right(x/y)divide102>>=(\r->Right(r*2))-- Right 10.0divide100>>=(\r->Right(r*2))-- Left "Divisão por zero!"
[] (List) Monad — não determinismo
Modela computações que podem ter múltiplos resultados (ou nenhum). O bind (>>=) executa a função para cada elemento da lista e concatena os resultados.
1
2
3
4
5
6
7
8
-- 'do' em Listas = produto cartesiano / "for aninhado"pairsxsys=dox<-xs-- Para cada x em xs...y<-ys-- ...para cada y em ys...return(x,y)-- ...produza (x, y)pairs[1,2][3,4]-- Resulta em: [(1,3),(1,4),(2,3),(2,4)]
Há, aqui, um código em Haskell para a esforçada leitora explorar os conceitos que acabamos de ver.
IO Monad — efeitos colaterais puros
A linguagem Haskell é, por definição, puramente funcional. Isso significa que uma função não pode modificar o estado global do programa, nem depender de efeitos externos. Em termos matemáticos, cada função é um morfismo entre objetos (tipos), obedecendo à propriedade fundamental da pureza: a mesma entrada sempre gera a mesma saída.
Mas então surge uma questão inevitável: como uma linguagem puramente funcional pode interagir com o mundo externo, que é essencialmente impuro?
Como ler uma entrada, escrever na tela, acessar o sistema de arquivos, ou gerar um número aleatório sem quebrar a pureza funcional?
A resposta categórica é a Monad IO.
IO como Functor, Applicative e Monad
O tipo IO a não representa o valor a, mas uma descrição pura de uma computação que, quando executada, produzirá um valor de tipo a e possivelmente causará efeitos colaterais. Dessa forma, o programa em Haskell não executa ações diretamente, ele constrói uma árvore de ações que o runtime do Haskell (GHC) executará posteriormente, fora do domínio puro da linguagem.
A IO é uma instância das três abstrações fundamentais:
1
2
3
4
5
6
7
8
9
10
11
12
instanceFunctorIOwherefmapfio=io>>=(return.f)instanceApplicativeIOwherepure=returnmf<*>mx=dof<-mfx<-mxreturn(fx)instanceMonadIOwhere(>>=)=bindIO-- definida internamente no runtime
Essas instâncias garantem que o comportamento de IO preserve as leis fundamentais de composição da teoria das categorias, permitindo combinar ações sequencialmente sem violar a pureza.
Estrutura categórica
Em termos formais, podemos enxergar IO como um endofunctor, onde:
Os objetos são tipos puros de Haskell (como Int, String, ());
Os morfismos são funções do tipo a -> IO b;
A unidade é a função return;
A multiplicação é a operação join, que achata camadas de ações encadeadas.
Essa estrutura obedece às leis das Monads:
Identidade à esquerda:return a >>= f ≡ f a
Identidade à direita:m >>= return ≡ m
Associatividade:(m >>= f) >>= g ≡ m >>= (\x -> f x >>= g)
Essas leis asseguram que, embora as ações tenham efeitos colaterais, a composição delas seja puramente determinística no nível semântico.
Encadeamento de ações com IO
O operador (>>=) (bind) é o responsável por encadear ações de I/O, garantindo a ordem explícita de execução. A atenta leitora pode considerar um exemplo clássico de interação com o usuário:
1
2
3
4
5
main::IO()main=doputStrLn"Qual é o seu nome?"nome<-getLineputStrLn("Olá, "++nome++"!")
O código acima é matematicamente equivalente a:
1
2
3
4
5
main_alt::IO()main_alt=putStrLn"Qual é o seu nome?">>=\_->getLine>>=\nome->putStrLn("Olá, "++nome++"!")
Observe que putStrLn e getLine são morfismos do tipo:
1
2
putStrLn::String->IO()getLine::IOString
Cada linha dentro do bloco do é, na verdade, uma composição monádica. A notação do é apenas uma forma conveniente de encadear operações que retornam IO.
Separação entre descrição e execução
A pureza é preservada porque a execução das ações não ocorre dentro da função, ela está apenas descrita. O runtime do Haskell é o responsável por interpretar essa descrição, realizando os efeitos colaterais no mundo real.
Isso significa que, matematicamente, cada ação IO a é um elemento de uma categoria de Kleisli associada à Monad IO:
Essa categoria permite que componhamos funções impuras, com efeitos, de maneira pura, através do operador (>=>):
saudacao::String->IO()saudacaonome=putStrLn("Olá, "++nome++"!")obterNome::IOStringobterNome=doputStrLn"Digite seu nome:"getLineprograma::IO()programa=obterNome>>=saudacao-- Ou, de forma categórica:-- programa = obterNome >=> saudacao
Combinando efeitos
O poder da IO Monad aparece ao compor várias ações que produzem e consomem dados.
Por exemplo, podemos construir um pequeno programa interativo que lê números, os processa e exibe resultados:
1
2
3
4
5
6
7
8
9
10
11
lerNumero::String->IOIntlerNumeroprompt=doputStrLnpromptinput<-getLinereturn(readinput)somaNumeros::IO()somaNumeros=dox<-lerNumero"Digite o primeiro número:"y<-lerNumero"Digite o segundo número:"putStrLn("A soma é: "++show(x+y))
Nesse caso, cada chamada de lerNumero é um morfismo () -> IO Int, e somaNumeros é a composição monádica dessas ações.
Do ponto de vista matemático, estamos compondo morfismos dentro da categoria de Kleisli de IO:
Composição e transformação de ações
Além do encadeamento sequencial, podemos transformar o resultado de ações IO com fmap e <*>, pois IO também é um Functor e um Applicative.
1
2
3
4
5
dobrarEntrada::IO()dobrarEntrada=doputStrLn"Digite um número:"n<-readLnprint(n*2)
Pode ser reescrito usando composição funcional pura:
Esses exemplos ilustram que a Monad IO é compatível com o restante da hierarquia Functor–Applicative–Monad, mantendo as mesmas propriedades de composição funcional.
Reflexão categórica final
Do ponto de vista categórico, IO é uma monad de efeitos, cuja interpretação semântica é dada pelo functor de Kleisli:
Através de bind e return, podemos compor ações de modo associativo, mantendo a semântica pura no nível das transformações de tipos. Assim, o Haskell preserva a pureza da função matemática, enquanto expressa programas que interagem com o mundo real.
Em outras palavras, IO não quebra a pureza de Haskell — ela a estende ao domínio dos efeitos, fornecendo uma ponte entre o cálculo funcional puro e a realidade impura da execução.
A Jornada da Abstração
A jornada que a atenta leitora percorreu:
Categorias → Functores → Applicatives → Monads é a espinha dorsal da programação funcional moderna.
A Teoria das Categorias não é uma abstração gratuita; ela fornece o vocabulário e as leis que garantem composicionalidade e segurança.
Conceito Matemático
Estrutura em Haskell
Exemplo
Objeto
Tipo
Int
Morfismo
Função pura
(+1)
Functor (endoFunctor)
Functor
fmap (+1) (Just 5)
Unidade ()
pure / return
pure 5 :: Maybe Int
Multiplicação ()
join
join (Just (Just 5))
Monad
Monad com (>>=)
Just 5 >>= return . (+1)
Finalmente podemos afirmar que Monads não são mágicas: são um padrão de design formal, baseado em matemática rigorosa, para sequenciar computações dependentes de forma pura, segura e composível.
Exercício 3
Monad Laws: Usando a definição da Maybe monad, prove a “Identidade à Esquerda” (return a >>= f == f a).
do-notation: Reescreva a seguinte expressão usando do-notation: safeDiv 100 2 >>= (\a -> safeDiv a 5 >>= (\b -> return (b + 1)))
List Monad: O que a seguinte expressão do calcula?
A leitora curiosa que chegou até aqui merece ver as soluções detalhadas, com explicações passo a passo, rigor matemático e código funcional. A seguir, apresentamos as respostas completas para todos os exercícios propostos, mantendo o mesmo tom didático e formal do texto principal.
Exercício 1
1. Análise das Leis: Por que a lei da identidade é definida como ? Explique por que (por exemplo) não faria sentido em termos de tipos.
Resposta:
A lei da identidade em uma categoria é definida em duas partes:
Isso garante que o morfismo identidade seja neutro em relação à composição, tanto à esquerda quanto à direita.
Considere os tipos em Hask:
1
2
3
f::a->bid_A::a->aid_B::b->b
A composição é definida como:
1
(.)::(b->c)->(a->b)->(a->c)
Agora, analise:
:
1
id_B.f::(a->b)->(b->b)->(a->b)
Tipo correto: (a -> b), igual ao tipo de f.
:
1
f.id_A::(a->a)->(a->b)->(a->b)
Tipo correto: (a -> b), igual ao tipo de f.
Mas e se tentássemos ?
1
id_A.f::(a->b)->(a->a)->(a->a)-- erro!
O operador (.) exige que o tipo de saída do segundo argumento seja igual ao tipo de entrada do primeiro. Aqui:
f :: a -> b
id_A :: a -> a
A saída de f é b, mas id_A espera a → incompatibilidade de tipos.
Portanto, não é bem-tipado em Hask, e por isso não faz parte da lei da identidade. A lei só inclui composições válidas.
2. Morfismos: Na categoria Hask, a função read :: String -> Int é um morfismo válido? Justifique considerando a definição de morfismo em Hask.
Resposta:
Em Hask (a categoria idealizada), os morfismos são funções puras e totais — ou seja, definidas para todos os valores do tipo de entrada, sem exceções ou loops infinitos.
A função:
1
read::String->Int
não é total. Por exemplo:
1
2
3
read"oi"-- lança exceção: Prelude.read: no parseread""-- exceçãoread"3.14"-- exceção
Essas entradas válidas do tipo String causam falha em tempo de execução. Portanto, readviola a propriedade de totalidade.
Conclusão: readnão é um morfismo válido na categoria Hask ideal.
Na prática, usamos Maybe ou Either para torná-la total:
1. Defina as assinaturas de tipo e implemente exemplos de uso para pure, Just e Maybe.
Resposta:
1
2
3
4
5
6
7
8
-- pure: injeta um valor puro em um contexto Applicativepure::Applicativef=>a->fa-- Just: construtor de Maybe que representa sucessoJust::a->Maybea-- Maybe: tipo que modela computação que pode falhardataMaybea=Nothing|Justa
Exemplos de uso:
1
2
3
4
5
6
7
8
9
10
11
pure42::MaybeInt-- Just 42pure(+)::Maybe(Int->Int->Int)-- Just (+)Just"Haskell"::MaybeString-- Just "Haskell"pureJust<*>Just10::Maybe(MaybeInt)-- Just (Just 10)
2. Leis do Functor: Prove que a implementação de fmap para Maybe obedece às duas leis.
concatThree::MaybeStringconcatThree=pure(++)<*>(pure(++)<*>val1<*>val2)<*>val3-- Ou, mais legível:concatThree=pure((++).(++))<*>val1<*>val2<*>val3-- Resultado: Just "abc"
Alternativa com liftA3:
1
2
3
4
importControl.Applicative(liftA3)concatThree=liftA3(\abc->a++b++c)val1val2val3-- Just "abc"
Exercício 3
1. Monad Laws: Prove a “Identidade à Esquerda” para Maybe.
1
returna>>=f==fa
Definição de return e (>>=) para Maybe:
1
2
3
4
returnx=JustxNothing>>=_=Nothing(Justx)>>=f=fx
Caso return a:
1
returna>>=f=Justa>>=f=fa
Conclusão: igual a f a.
2. do-notation: Reescreva usando do.
1
safeDiv1002>>=(\a->safeDiva5>>=(\b->return(b+1)))
Resposta:
1
2
3
4
5
result::MaybeIntresult=doa<-safeDiv1002-- a = Just 50b<-safeDiva5-- b = Just 10return(b+1)-- Just 11
3. List Monad: O que a expressão calcula?
1
2
3
4
don<-[1,2,3]guard(oddn)return(n*10)
Resposta:
guard b retorna [()] se b == True, ou [] se b == False.
Passo a passo:
n <- [1,2,3] → tenta n = 1, n = 2, n = 3
guard (odd n):
n=1: odd 1 = True → [()]
n=2: odd 2 = False → [] → elimina este ramo
n=3: odd 3 = True → [()]
return (n * 10):
Para n=1: [1 * 10] = [10]
Para n=3: [3 * 10] = [30]
Resultado final: [10, 30]
Ou seja: os números ímpares da lista original, multiplicados por 10.