Teoria das Categorias e Monads em Haskell

por Frank de Alcantara em 10/11/2025

Teoria das Categorias e Monads em Haskell

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 C é uma estrutura matemática, similar a um grafo direcionado, composta por:

  • Objetos: O b j ( C ) . A cuidadosa leitora pode interpretá-los como tipos de dados (ex: Int, String) ou conjuntos.
  • Morfismos: H o m C ( A , B ) . São as “setas” ou transformações entre objetos (ex: funções, como show :: Int -> String).
  • Operação de composição: ∘ : H o m ( B , C ) → H o m ( A , B ) → H o m ( A , C ) . Uma operação que combina morfismos de forma associativa.
  • Morfismo identidade: i d A : A → A . Para cada objeto A , existe uma seta que não faz nada.

Esta estrutura pode ser vista na figura a seguir:

Diagrama de uma categoria com dos objetos, A e B dois morfimos representados por setas e a operação de composição

Para que essa estrutura seja formalmente uma categoria, duas leis são necessárias e indispensáveis:

  1. Associatividade: Para morfismos f : A → B , g : B → C e h : C → D : h ∘ ( g ∘ f ) = ( h ∘ g ) ∘ f

  2. Identidade: Para qualquer morfismo f : A → B : i d B ∘ f = f = f ∘ i d A

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

-- Identidade
f . id == f -- à direita
id . f == f -- à esquerda

A figura a seguir ilustra a categoria Hask:

Diagrama da categoria Hask representando um morfismo de `Int` para `String`, um morfismo de `String` para `Bool` e a assinatura de tipos da operação de composição

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: { 1 , 2 , 3 } , R , { vermelho , azul } ).
Morfismos Funções (totais) entre conjuntos (ex: f : Z → Z definida por f ( x ) = x 2 ).
Composição A composição padrão de funções, ( g ∘ f ) ( x ) = g ( f ( x ) ) .
Identidade A função identidade i d A ( x ) = x para cada conjunto A .

As leis da categoria são satisfeitas:

  1. Associatividade: A composição de funções é inerentemente associativa, h ∘ ( g ∘ f ) = ( h ∘ g ) ∘ f .
  2. Identidade: A função i d atua como elemento neutro, f ∘ i d A = f e i d B ∘ f = f .

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 ( P , ≤ ) um conjunto P com uma relação de ordem parcial ≤ , reflexiva, antissimétrica e transitiva. Neste caso, teremos:

Elemento Em Poset
Objetos Os elementos do conjunto P (ex: 1 , 2 , 3 , … ).
Morfismos A própria relação. Existe um morfismo f : A → B se, e somente se, A ≤ B .
Composição A transitividade da relação.
Identidade A reflexividade da relação.

Para que esta seja uma categoria, as leis devem ser satisfeitas:

  1. Associatividade: se existe um morfismo f : A → B (ou seja, A ≤ B ) e g : B → C (ou seja, B ≤ C ), a composição g ∘ f exige um morfismo h : A → C . Neste cenário, a propriedade da transitividade ( A ≤ B e B ≤ C ⟹ A ≤ C ) garante que este morfismo h existe.
  2. Identidade: para todo objeto A , deve existir um morfismo i d A : A → A . A propriedade da reflexividade ( A ≤ A ) garante que este morfismo de identidade sempre existe.

Exercício 1

  1. Análise das Leis: Por que a lei da identidade é definida como i d B ∘ f = f = f ∘ i d A ? Explique por que i d A ∘ f (por exemplo) não faria sentido em termos de tipos.
  2. 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 F : C → D é um mapeamento que preserva a estrutura das categorias C e D :

  • Mapeia Objetos: A ∈ C ↦ F ( A ) ∈ D
  • Mapeia Morfismos: ( f : A → B ) ↦ ( F ( f ) : F ( A ) → F ( B ) )

Este mapeamento deve obedecer a duas leis:

  1. Preservação da identidade: F ( i d A ) = i d F ( A )

  2. Preservação da composição: F ( g ∘ f ) = F ( g ) ∘ F ( f )

Functores em Haskell

Em Haskell, quase sempre lidamos com endofunctores, functores que mapeiam Hask para Hask. A typeclass Functor captura essa ideia:

1
2
3
4
5
6
7
class Functor f where\
fmap :: (a -> b) -> f a -> f b

-- `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`).

Exemplos:

1
2
3
fmap (+10) (Just 5) -- Resulta em: Just 15
fmap (+1) [1,2,3] -- Resulta em: [2,3,4]
fmap length ["a","bc"] -- Resulta em: [1,2]

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\
fmap id == id

-- Preservação da composição\
fmap (g . f) == fmap g . fmap f

ilustra hierarquia e assinaturas importantes; coloque esta figura onde inicia aplicative para deixar claro a progressão conceitual.

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)\
Just 10 :: Maybe Int

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 typeclass Applicative, que estende a typeclass Functor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b

-- `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) <*> Just 10 -- Resulta em: Just 15

-- E se quisermos somar dois valores em contexto?

pure (+) <*> Just 5 <*> Just 10 -- Resulta em: Just 15

-- Ele também propaga falhas (Nothing)

pure (+) <*> Just 5 <*> Nothing -- Resulta em: Nothing

A typeclass Applicative é 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 (+) <*> Just 5 <*> Just 10
-- 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 (+) <*> Just 5 <*> Nothing
-- Resultado: Nothing

Da mesma forma, o comportamento se estende a listas, que representam computações com múltiplos resultados possíveis:

1
2
pure (*) <*> [1, 2] <*> [10, 100]
-- Resultado: [10, 100, 20, 200]

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

  1. Em Haskell, defina as assinaturas de tipo e implemente exemplos de uso para as funções pure, just e Maybe.

  2. Leis do Functor: Prove que a implementação de fmap para Maybe obedece às duas leis do functor.

1
2
3
  instance Functor Maybe where
  fmap _ Nothing = Nothing
  fmap f (Just x) = Just (f x)
  1. 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.

  2. 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.

Considere este cenário:

1
2
buscarUsuario :: String -> Maybe User
buscarPermissoes :: User -> Maybe Permissions

Não podemos usar (<*>). Precisamos do valor User de 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 C é uma tripla ( T , η , μ ) que consiste em:

  1. T : C → C — um endoFunctor (ex: Maybe).

  2. η : I → T — uma transformação natural chamada unidade (pure/return). Ela pega um objeto A e o “injeta” no Functor, A → T ( A ) .

  3. μ : T 2 → T — uma transformação natural chamada multiplicação (join). Ela “achata” um Functor aninhado, T ( T ( A ) ) → T ( A ) .

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 typeclass Monad é:

1
2
3
class Applicative m => Monad m where
  (>>=) :: m a -> (a -> m b) -> m b
-- return = pure (não é mais parte da classe)

A atenta leitora deve prestar a atenção à dois pontos importantes:

  1. Toda Monad é também Applicativee, portanto, Functor.
  2. 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
(>>=) :: m a -> (a -> m b) -> m b

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:\
Just 5 >>= (\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:

  1. 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 a f :: a -> m b fmap f m :: m (m b) (Contexto aninhado!) join (...) :: m b (Achatado!)

  1. join em termos de bind Em Haskell, podemos definir join facilmente:
1
2
  join :: Monad m => m (m a) -> m a\
  join mma = 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 C define uma nova categoria, chamada Categoria de Kleisli, denotada por C T , onde T é 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 ( T , η , μ ) em C :

  • Objetos: são os mesmos objetos de C .
  • Morfismos: para objetos A e B , temos
    H o m C T ( A , B ) = H o m C ( A , T B )
    ou seja, as setas em C T são funções do tipo A → T B .
  • Composição: é definida usando a operação bind, ou equivalentemente μ e o funtor T :
    g ∘ T f = μ B ∘ T ( g ) ∘ f

    visualiza claramente as setas A - srcset= T B e B -> T C e sua composição Kleisli.">

    Em Haskell, essa composição é implementada pelo operador (>=>):

    1
    2
    
    (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
    f >=> g = \x -> f x >>= g
    
  • Identidade: para cada objeto A , a identidade é dada por
    i d A T = η A : A → T A ,
    que em Haskell corresponde a return.

Intuição: composição de computações com efeitos

Na categoria original C , funções puras compõem-se normalmente: f : A → B , g : B → C ⟹ g ∘ f : A → C

Na categoria de Kleisli, as setas são funções que retornam valores em contexto: f : A → T B , g : B → T C

Como não podemos compor diretamente f e g, precisamos da estrutura monádica para “encadear” essas computações: g ∘ T f = λ x . f ( x ) >>= g

Assim, (>=>) é a composição categórica em C T . 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

  1. Com Maybe
1
2
3
4
5
6
7
8
9
f :: Int -> Maybe String
f x = if x > 0 then Just (show x) else Nothing

g :: String -> Maybe Int
g s = if length s < 3 then Just (read s + 1) else Nothing

-- Composição monádica (Kleisli)
h :: Int -> Maybe Int
h = f >=> g

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.

  1. Com IO
1
2
3
4
5
6
7
8
9
10
lerNumero :: IO Int
lerNumero = do
  putStrLn "Digite um número:"
  readLn

mostrarDobro :: Int -> IO ()
mostrarDobro n = 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 C Categoria de Kleisli C T
Morfismo A → B A → T B
Identidade i d A η A (return)
Composição g ∘ f μ ∘ T ( g ) ∘ f (>=>)

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 = do
  x <- Just 5
  y <- Just 10
  return (x + y) -- Resulta em: Just 15

É traduzido pelo compilador para este código com (>>=):

1
2
3
4
main_bind =
  Just 5 >>= (\x  -> 
    Just 10 >>= (\y  -> 
      return (x + y)))

Exemplos de Monads Importantes

Maybe Monad — falhas controladas

Modela computações que podem falhar, propagando Nothing (short-circuit) automaticamente.

1
2
3
4
5
6
7
8
9
safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv x y = Just (x `div` y)

program = do
  a <- safeDiv 10 2 -- a = 5
  b <- safeDiv a 0 -- b = Nothing
  c <- safeDiv b 1 -- Esta linha nunca executa
return (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 -> Either String Double
divide _ 0 = Left "Divisão por zero!"
divide x y = Right (x / y)

divide 10 2 >>= (\r -> Right (r * 2)) -- Right 10.0
divide 10 0 >>= (\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"
pairs xs ys = do
  x <- 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
instance Functor IO where
  fmap f io = io >>= (return . f)

instance Applicative IO where
  pure = return
  mf <*> mx = do
    f <- mf
    x <- mx
    return (f x)

instance Monad IO where
  (>>=) = 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 T : C → C , 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 → T ( A ) é a função return;
  • A multiplicação μ : T ( T ( A ) ) → T ( A ) é a operação join, que achata camadas de ações encadeadas.

Essa estrutura obedece às leis das Monads:

  1. Identidade à esquerda: return a >>= f ≡ f a
  2. Identidade à direita: m >>= return ≡ m
  3. 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 = do
  putStrLn "Qual é o seu nome?"
  nome <- getLine
  putStrLn ("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  :: IO String

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:

Kleisli ( IO ) : A → f IO  B

Essa categoria permite que componhamos funções impuras, com efeitos, de maneira pura, através do operador (>=>):

1
2
(>=>) :: (a -> IO b) -> (b -> IO c) -> (a -> IO c)
f >=> g = \x -> f x >>= g

Por exemplo:

1
2
3
4
5
6
7
8
9
10
11
12
saudacao :: String -> IO ()
saudacao nome = putStrLn ("Olá, " ++ nome ++ "!")

obterNome :: IO String
obterNome = do
  putStrLn "Digite seu nome:"
  getLine

programa :: 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 -> IO Int
lerNumero prompt = do
  putStrLn prompt
  input <- getLine
  return (read input)

somaNumeros :: IO ()
somaNumeros = do
  x <- 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:

( ) → l e r N u m e r o IO Int → f m a p ( + ) IO (Int -> Int) → < ∗ > IO Int

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 = do
  putStrLn "Digite um número:"
  n <- readLn
  print (n * 2)

Pode ser reescrito usando composição funcional pura:

1
2
dobrarEntrada' :: IO ()
dobrarEntrada' = fmap (*2) readLn >>= print

Ou ainda, com aplicação dentro do contexto:

1
2
dobrarEntrada'' :: IO ()
dobrarEntrada'' = print =<< fmap (*2) readLn

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:

T = ( ações que produzem efeitos ) : C → C

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

  1. Monad Laws: Usando a definição da Maybe monad, prove a “Identidade à Esquerda” (return a >>= f == f a).

  2. do-notation: Reescreva a seguinte expressão usando do-notation:
    safeDiv 100 2 >>= (\a -> safeDiv a 5 >>= (\b -> return (b + 1)))

  3. List Monad: O que a seguinte expressão do calcula?

1
2
3
4
 do 
 n <- [1, 2, 3] 
 guard (odd n) -- guard :: Bool -> [()] 
 return (n * 10)

(Dica: guard True = [()], guard False = [])

Resposta dos Exercícios

Mostrar respostas dos exercícios

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 i d B ∘ f = f = f ∘ i d A ? Explique por que i d A ∘ f (por exemplo) não faria sentido em termos de tipos.

Resposta:

A lei da identidade em uma categoria é definida em duas partes:

  • i d B ∘ f = f
  • f ∘ i d A = f

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 -> b
id_A  :: a -> a
id_B  :: b -> b

A composição é definida como:

1
(.) :: (b -> c) -> (a -> b) -> (a -> c)

Agora, analise:

  • i d B ∘ f :

    1
    
    id_B . f :: (a -> b) -> (b -> b) -> (a -> b)
    

    Tipo correto: (a -> b), igual ao tipo de f.

  • f ∘ i d A :

    1
    
    f . id_A :: (a -> a) -> (a -> b) -> (a -> b)
    

    Tipo correto: (a -> b), igual ao tipo de f.

Mas e se tentássemos i d A ∘ f ?

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, i d A ∘ f 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 parse
read ""     -- exceção
read "3.14" -- exceção

Essas entradas válidas do tipo String causam falha em tempo de execução. Portanto, read viola a propriedade de totalidade.

Conclusão: read não é um morfismo válido na categoria Hask ideal.
Na prática, usamos Maybe ou Either para torná-la total:

1
2
3
4
safeRead :: String -> Maybe Int
safeRead s = case reads s of
  [(x, "")] -> Just x
  _         -> Nothing

Agora sim, safeRead é um morfismo em Hask.

Exercício 2

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 Applicative
pure :: Applicative f => a -> f a

-- Just: construtor de Maybe que representa sucesso
Just :: a -> Maybe a

-- Maybe: tipo que modela computação que pode falhar
data Maybe a = Nothing | Just a

Exemplos de uso:

1
2
3
4
5
6
7
8
9
10
11
pure 42 :: Maybe Int
-- Just 42

pure (+) :: Maybe (Int -> Int -> Int)
-- Just (+)

Just "Haskell" :: Maybe String
-- Just "Haskell"

pure Just <*> Just 10 :: Maybe (Maybe Int)
-- Just (Just 10)

2. Leis do Functor: Prove que a implementação de fmap para Maybe obedece às duas leis.

1
2
3
instance Functor Maybe where
  fmap _ Nothing   = Nothing
  fmap f (Just x)  = Just (f x)

Lei 1: Preservação da identidade

f m a p   i d = i d

  • Caso Nothing:

    1
    
    fmap id Nothing = Nothing = id Nothing
    
  • Caso Just x:

    1
    
    fmap id (Just x) = Just (id x) = Just x = id (Just x)
    

Conclusão: válida.

Lei 2: Preservação da composição

f m a p   ( g . f ) = f m a p   g . f m a p   f

  • Caso Nothing:

    1
    2
    
    fmap (g . f) Nothing = Nothing
    (fmap g . fmap f) Nothing = fmap g Nothing = Nothing
    
  • Caso Just x:

    1
    2
    
    fmap (g . f) (Just x) = Just ((g . f) x) = Just (g (f x))
    (fmap g . fmap f) (Just x) = fmap g (Just (f x)) = Just (g (f x))
    

Conclusão: válida.

3. Leis do Functor para Listas: Prove a lei da composição.

1
fmap (g . f) == fmap g . fmap f

Em listas, fmap = map.

Prova por indução estrutural:

  • Caso base: lista vazia []

    1
    2
    
    map (g . f) [] = []
    (map g . map f) [] = map g [] = []
    
  • Caso indutivo: x:xs

    1
    2
    
    map (g . f) (x:xs) = (g (f x)) : map (g . f) xs
    (map g . map f) (x:xs) = map g (f x : map f xs) = g (f x) : map g (map f xs)
    

    Pela hipótese indutiva, map (g . f) xs = map g (map f xs) → igual.

Conclusão: válida.

4. Uso de Applicative: Combine três Maybe String em um Maybe String (concatenando).

1
2
3
4
val1, val2, val3 :: Maybe String
val1 = Just "a"
val2 = Just "b"
val3 = Just "c"

Solução:

1
2
3
4
5
concatThree :: Maybe String
concatThree = pure (++) <*> (pure (++) <*> val1 <*> val2) <*> val3
-- Ou, mais legível:
concatThree = pure ((++) . (++)) <*> val1 <*> val2 <*> val3
-- Resultado: Just "abc"

Alternativa com liftA3:

1
2
3
4
import Control.Applicative (liftA3)

concatThree = liftA3 (\a b c -> a ++ b ++ c) val1 val2 val3
-- Just "abc"

Exercício 3

1. Monad Laws: Prove a “Identidade à Esquerda” para Maybe.

1
return a >>= f == f a

Definição de return e (>>=) para Maybe:

1
2
3
4
return x = Just x

Nothing  >>= _ = Nothing
(Just x) >>= f = f x
  • Caso return a:

    1
    
    return a >>= f = Just a >>= f = f a
    

Conclusão: igual a f a.

2. do-notation: Reescreva usando do.

1
safeDiv 100 2 >>= (\a -> safeDiv a 5 >>= (\b -> return (b + 1)))

Resposta:

1
2
3
4
5
result :: Maybe Int
result = do
  a <- safeDiv 100 2  -- a = Just 50
  b <- safeDiv a 5     -- b = Just 10
  return (b + 1)       -- Just 11

3. List Monad: O que a expressão calcula?

1
2
3
4
do 
  n <- [1, 2, 3] 
  guard (odd n)
  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.

(Updated: )