
Desenvolvendo nosso interesse incansável pela literatura acadêmica séria , pode-se dizer, chegamos à teoria das categorias . Este tópico, na famosa apresentação de Bartosz Milevsky, já apareceu em Habré e agora pode se orgulhar de tais indicadores: É tanto mais agradável que conseguimos encontrar um material relativamente novo (janeiro de 2020), que serve como uma introdução excelente e ao mesmo tempo tão breve quanto possível à teoria das categorias. Esperamos poder interessar você neste tópico.

Se você e eu, caro leitor, enfrentamos problemas semelhantes, você já foi atormentado pela pergunta "o que diabos é uma mônada?!" Então você pesquisou essa questão no Google, deslizando furtivamente pela toca do coelho da matemática abstrata , emaranhando-se em functores , monóides , categoriasaté que perceberam que já haviam esquecido a pergunta que você trouxe aqui. Essa experiência pode ser bastante opressiva se você nunca viu linguagens de programação funcionais antes, mas não se preocupe! Estudei muitas páginas de matemática densa para você e assisti a horas de palestras sobre esse assunto. Portanto, para evitar essa necessidade, vou resumir o tópico aqui e também mostrar como você pode aplicar a teoria das categorias para que você possa pensar (e escrever código) em um estilo funcional agora.
Este artigo é destinado a qualquer pessoa que se considere um "novato" no campo da programação funcional e esteja apenas começando a usar Scala, Haskell ou outra linguagem semelhante. Depois de ler este artigo, você se sentirá um pouco mais confiante ao interpretar os fundamentos da teoria das categorias e identificar seus princípios "no terreno". Além disso, se você já experimentou matemática teórica, evite mencionar diretamente os conceitos que são discutidos aqui. Via de regra, muito mais pode ser dito sobre cada um deles do que está escrito aqui, mas este artigo será suficiente para um programador curioso.
O básico
Então, o que exatamente é uma categoria e como ela se relaciona com a programação? Como muitos conceitos usados em programação, uma categoria é uma coisa muito simples com um nome sofisticado. Este é apenas um gráfico direcionado rotulado com algumas restrições adicionais. Cada um dos nós em uma categoria é chamado de "objeto" e cada uma de suas arestas é chamada de "morfismo".

Como você deve ter adivinhado, nem todo gráfico direcionado é uma categoria; para que um gráfico seja considerado uma categoria, alguns critérios adicionais devem ser atendidos. Na próxima foto, notamos que cada objeto possui um morfismo que aponta para si mesmo. Este é um morfismo idêntico, e cada objeto deve possuir tal que o gráfico seja considerado uma categoria. Em seguida, observe que o objeto
A
tem um morfismo f
indicandoB
, e, da mesma forma, o objeto B
tem um morfismo g
apontando para C
. Uma vez que existe um caminho a partir A
de B
e para B
a C
, obviamente, há um caminho a partir A
de C
, certo? Este é o próximo requisito de categorias para morfismos que necessariamente a composição associativa deve ser realizada, de modo que para um morfismo f: A = > B
, e g: B = > C
haja um morfismo h = g(f): A = > C
.
Esses cálculos já podem parecer um pouco abstratos, então vamos ver um exemplo que satisfaz essa definição e foi escrito em Scala.
trait Rock
trait Sand
trait Glass
def crush(rock: Rock): Sand
def heat(sand: Sand): Glass
Acho que esse exemplo torna o relacionamento um pouco mais fácil. Apagar pedras em areia é um morfismo que transforma um objeto
rock
em objeto sand
, enquanto fundir vidro de areia é um morfismo que transforma um objeto sand
em objeto glass
. Neste caso, a composição dessas relações definitivamente se parecerá com
val glass: Glass = heat(crush(rock))
Existem também funções de identidade (definidas em
Predef
Scala), pois para qualquer objeto não é difícil escrever uma função que retorne o mesmo objeto. Portanto, este sistema é uma categoria, embora bastante simples.
Maior familiaridade com as categorias

Agora vamos nos aprofundar um pouco mais na terminologia da teoria das categorias e começar com uma categoria chamada magma. Se você ainda não está muito familiarizado com este conceito básico, deixe-me explicar que o magma é apenas uma operação binária, ou seja, uma operação em dois valores, como resultado da qual um novo valor é obtido. Para não entrar em detalhes, não vou dar aqui uma prova de que o conjunto de todas as operações binárias é na verdade uma categoria, mas para aqueles que estão interessados em detalhes, recomendo a leitura do seguinte artigo de Bartosz Milevsky. Toda a aritmética da adição à multiplicação pertence às subcategorias, unidas pela categoria magmática (ver diagrama).
A seguinte ordem de herança se aplica aqui:
- 1. Magma: todas as operações binárias
- 2. Semigrupos: todas as operações binárias que são associativas
- o : .
- 3. : ,
- o : , (aka )
Portanto, voltando ao nosso exemplo anterior, tanto a adição quanto a multiplicação são monóides porque são associativas
(a + (b + c) = (a + b) + c)
e têm um único elemento (ax 1 = 1 xa = a). O último círculo neste diagrama contém quase-grupos para os quais podem ser aplicados diferentes princípios de incorporação do que para semigrupos ou monóides. Quasigrupos são operações binárias reversíveis. Explicar essa propriedade não é tão fácil, portanto, indico-lhe a série de artigos de Mark Siman dedicados a esse tópico. Uma operação binária é reversível quando para quaisquer valores a
e b
existem tais valores x
e y
que permitem a conversão a
parab
... Eu sei que parece complicado. Para esclarecer, vamos dar uma olhada no seguinte exemplo de subtração:
val x = a - b
val y = a + b
assert(a - x == b)
assert(y - a == b)
Observação: ele
y
não participa da subtração como tal, mas o exemplo ainda conta. Os objetos em uma categoria são abstraídos e podem ser quase qualquer coisa; neste caso, é importante que para qualquer a
e b
que possa ser gerado, essas afirmações permaneçam verdadeiras.
Tipos em contexto
Independentemente de sua disciplina específica, o tópico de tipos deve ser claro para qualquer pessoa que entenda o significado dos tipos de dados na programação. Inteiro, booleano, ponto flutuante e assim por diante são todos tipos, mas como você descreveria o tipo platônico ideal em palavras? Em seu livro Category Theory for Programmers, que se transformou em uma série de postagens de blog, Milevsky descreve os tipos simplesmente como "conjuntos de valores". Por exemplo, um booleano é um conjunto finito que contém os valores “verdadeiro” e “falso” (falso). Um caractere (char) é um conjunto finito de todos os caracteres numéricos e uma string (string) é um conjunto infinito de char.
O problema é que, na teoria das categorias, tendemos a nos afastar dos conjuntos e pensar em termos de objetos e morfismos. Mas o fato de que os tipos são simplesmente conjuntos é inevitável. Felizmente, há um lugar na teoria das categorias para esses conjuntos, uma vez que nossos objetos são abstrações e podem representar qualquer coisa. Portanto, temos todo o direito de dizer que nossos objetos são conjuntos e, além disso, considerar nossos programas Scala como categorias, onde os tipos são objetos e as funções são morfismos. Para muitos, isso pode parecer dolorosamente óbvio; afinal, no Scala estamos acostumados a lidar com objetos, mas vale a pena apontar isso explicitamente.
Se você já trabalhou com uma linguagem orientada a objetos, como Java, provavelmente está familiarizado com o conceito de tipos genéricos. São coisas como
LinkedList
ou, no caso de Scala, Option[T]
onde T
representa o tipo de dados subjacente armazenado em alguma estrutura. E se quiséssemos criar um mapeamento de um tipo para outro para que a estrutura do primeiro tipo fosse preservada? Bem-vindo ao mundo dos functores, definidos como mapeamentos entre categorias para manter a estrutura. Na programação, você geralmente precisa trabalhar com uma subcategoria de functores, os chamados endofunctors, que ajudam a mapear uma categoria para si mesma. Então, vou apenas dizer que quando falo sobre functores, quero dizer endofunctors.
Como exemplo de um functor, vamos olhar para o tipo Scala
Option[T]
em conjunto com nosso exemplo anterior, que mencionou pedra, areia e vidro:
val rockOpt: Option[Rock] = Some(rock)
Acima temos o tipo
Rock
conforme definido anteriormente, mas incluído Option
. Este é um tipo genérico (e mais, mais sobre isso abaixo), nos dizendo que um objeto é uma entidade específica que estamos procurando ou None
que pode ser comparada null
em outras linguagens.
Se não usássemos functores, poderíamos imaginar como estamos aplicando uma função
crush()
a Rock
, o que exigiria o recurso a um operador if
para lidar com a situação em que se Option
encontra None
.
var sandOpt: Option[Sand] = None
if(rockOpt != None) {
sandOpt = Some(crush(rockOpt.get))
}
Podemos dizer que isso é um aparte, mas por favor, não use var - esse código é ruim no Scala por vários motivos. Novamente, voltando ao tópico: em Java ou C #, isso não seria um problema. Você verifica se o seu valor é do tipo que você esperava ver e faz o que quiser com ele. Mas com o poder dos functores, tudo pode ser feito com um pouco mais de elegância com a função
map()
:
val sandOpt: Option[Sand] = rockOpt.map(crush)
Boom, uma linha e pronto. Seria possível colocar o primeiro exemplo em uma linha, usando o operador ternário ou algo semelhante, mas você não teria conseguido de forma tão sucinta. Este exemplo é verdadeiramente maravilhoso em sua simplicidade. Aqui está o que está acontecendo aqui: ele
map()
pega uma função e mapeia essa função (em um sentido matemático) para si mesmo. A estrutura é Option
preservada, mas agora contém ou Sand
, ou None
, em vez de Rock
ou None
. Isso pode ser ilustrado da seguinte forma:

Observe como tudo se alinha lindamente, cada objeto e morfismo são preservados em ambos os sistemas. Consequentemente, o morfismo no centro é um functor que representa um mapeamento de
T
a Option[T]
.
Juntos
Agora podemos finalmente voltar à questão original “o que diabos é uma mônada”? Existe uma resposta na qual você pode tropeçar cegamente se apenas tentar pesquisar no Google, e soa assim: uma mônada é apenas um monóide na categoria de endofunctors, que geralmente é seguida por uma observação muito sarcástica: "Qual é o problema?" Via de regra, tentam assim, de brincadeira, mostrar o quão difícil é tudo neste tópico, mas na verdade nem tudo é tão assustador - afinal, já descobrimos o que essa frase significa. Vamos dar um passo a passo novamente.
Primeiro, sabemos que os monóides são operações binárias associativas, cada uma das quais contém um elemento neutro (único). Em segundo lugar, sabemos que os endofunctors nos permitem mapear uma categoria para si mesma, enquanto mantemos a estrutura. Portanto, uma mônada é apenas algum tipo de invólucro (como no exemplo de tipo genérico acima) que mantém algum método para aceitar uma função e mapeá-la para si mesma.
List
- esta é uma mônada, Option
(como a que consideramos acima) é uma mônada, e alguém pode até te dizer isso e Future
também é uma mônada. Exemplo:
val l: List[Int] = List(1, 2, 3)
def f(i: Int) = List(i, i*2, i*3)
println(l.flatMap(f)) // : List(1, 2, 3, 2, 4, 6, 3, 6, 9)
Enganosamente simples, não é? No mínimo, deveria haver a sensação de que não é difícil apreender tudo aqui, mesmo que à primeira vista não esteja totalmente claro qual é o uso de tal conceito.