Esse post é baseado no livro Teoria das Categorias para Programadores, de Bartosz Milewski.
Vimos anteriormente em Tipos de Dados Algébricos os tipos fundamentais e a construção de tipos mais complexos através do Tipo Soma, do Tipo Produto, e Tipos Recursivos. Esses tipos são algébricos pois, ao denotá-los como variáveis representando a cardinalidade do conjunto de seus valores, tais estruturas podem ser formadas através de operações algébricas de soma e multiplicação.
Em Teoria das Categorias, para uma categoria \(C\) definimos \(C(a, b)\) como o conjunto de morfismos iniciando em \(a\) e terminando em \(b\). Na categoria \(\mathbf{Hask}\) temos que um morfismo é uma função que recebe um argumento do tipo \(a\) e retorna um tipo \(b\). Se \(a \rightarrow b\) representa o conjunto de funções com essa assinatura, podemos definir um Tipo Função.
Para definir o tipo função, vamos utilizar a construção universal. O padrão que buscamos dentro de nossa categoria é formado por três objetos, um representando o nosso tipo função que iremos chamar de \(z\), e que contém diversas funções \(f\), um que representa o argumento da função, o tipo \(a\), e outro que representa o resultado da aplicação da função, ou tipo \(b\):
A conexão entre esses três objetos se dá pelo padrão conhecido pelos
programadores como aplicação de função ou avaliação de função. Esse
padrão pode ser escrito como: dado um tipo função \(z\) e um tipo de
entrada \(a\), o morfismo mapeia essa tupla em um tipo \(b\). Ou seja,
temos um tipo produto formado por \((z, a)\) e um morfismo
g :: (z, a) -> b
.
Finalmente precisamos definir um rank para seleconar o melhor \(z\)
entre todos os candidatos. Para isso definimos que um \(z\) é melhor que
um \(z’\) caso exista um morfismo h :: z' -> z
que fatora \(g’\) em
\(g\), ou seja, podemos definir \(g’\) em função de \(h\).
Para isso a fatoração deve funcionar no par \((z’, a)\) transformando em
um par \((z, a)\). Já sabemos que a transformação de um tipo produto
(a, b)
para um tipo (c, d)
é feito através de um Bifunctor, nesse
nosso caso temos bimap = (h, id) :: (z', a) -> (z, a)
. Com isso
conseguimos definir g' = g . (h, id)
.
Finalmente vamos encontrar o melhor entre todos os candidatos de \(z\).
Vamos chamar o melhor \(z\) como \(a \implies b\) e o melhor \(g\) como
eval
(de function evaluation). Com isso temos
Um objeto função de a
para b
é denominado \(a \implies b\) junto com o morfismo eval :: ((a => b), a) -> b
tal que para qualquer outro objeto \(z\) com um morfismo g :: (z, a) -> b
existe um único morfismo h :: z -> (a => b)
que g = eval . (h, id)
.
Imagine que escolhemos um certo objeto \(z\) acompanhado de seu morfismo \(g\). O morfismo pode ser interepretado como uma função de dois argumentos \((z, a)\) que retorna um \(b\):
g :: (z, a) -> b
Sabemos que a melhor escolha de objeto pode ser encontrada através da
aplicação de \(h\), que transforma nosso \(z\) em um a -> b
:
h :: z -> (a -> b)
Dizemos que \(h\) é uma função que recebe um objeto do tipo \(z\) e
retorna uma função de \(a\) para \(b\), é uma função de alta ordem. Isso
nos diz que toda função de dois argumentos é, na verdade, uma função de
um argumento que retorna outra função. Isso é conhecido como currying,
dizemos que h
é a forma curried de g
. Podemos definir a forma
uncurried utilizando nosso morfismo eval
, que reconstrói nosso g
:
g = eval . (h, id) :: (z, a) -> b
Ou seja, essas definições são isomórficas. Em Haskell todas as versões de múltiplos argumentos são naturalmente interpretadas como sua versão curry:
a -> (b -> c) = a -> b -> c
Isso fica claro na definição de uma função de duas variáveis em Haskell:
mult :: Int -> Int -> Int
mult x y = x*y
mult' :: Int -> (Int -> Int)
mult' x = \y -> x*y
Que se torna evidente quando fazemos uma aplicação parcial da primeira função:
dobra = mult 2
dobra :: Int -> Int
A biblioteca padrão do Haskell já tem a conversão das duas formas de definição de funções:
curry :: ((a, b)->c) -> (a->b->c)
curry f a b = f (a, b)
uncurry :: (a->b->c) -> ((a, b)->c)
uncurry f (a, b) = f a b
Em C++ você pode fazer uma aplicação parcial de função utilizando o
template std::bind
:
int mult(int x, int y) {
return x*y;
}
using namespace std::placeholders;
auto dobra = std::bind(mult, 2, _1);
std::cout << dobra(4);
Em Python temos a função partial
:
from functools import partial
def mult(x,y):
return x * y
dobra = partial(mult, 2)
A interpretação algébrica de um tipo função é a de um exponencial. Isso
se torna evidente com uma função do tipo Bool
para um certo tipo a
e
outra função de um tipo a
para Bool
:
f :: Bool -> a
g :: a -> Bool
De quantas maneiras podemos definir a função f
? A entrada é definida
pelos dois possíveis valores True
e False
, ou seja, podemos dizer
que uma função de Bool
para a
é definida por um par de valores de
a
. Já vimos isso anteriormente quando percebemos que Bool -> a
é
isomórfica a (a, a)
. Com isso temos a^2
definições diferentes para
essa função.
A função g
deve definir um valor True
ou False
para cada um dos
valores de a
, ou seja, é uma tupla (Bool, Bool, Bool, ..., Bool)
com
a
elementos. Analogamente, isso é equivalente a 2^a
possíveis
definições.
O tipo função a -> b
é representada por uma exponencial b^a
,
indicando as possíveis combinações de entrada e saída para essa
assinatura de função.
Vamos verificar se esse tipo também obedece as propriedades algébricas de uma exponenciação.
Uma função \(a^0 = 1\) tem a assinatura:
f :: Void -> a
que já vimos possuir apenas uma definição que é a função absurd
.
Analogamente, uma função \(a^1 = a\) tem a assinatura:
f :: () -> a
que é a função unit
que seleciona um valor de a
, portanto possui a
definições diferentes.
Já a função \(1^a = 1\) tem como assinatura e única definição:
const :: a -> ()
const x = ()
Uma função \(a^{b+c}\) é uma função de um tipo soma para um tipo a
:
f :: Either b c -> a
f (Left x) = ...
f (Right y) = ...
Ou seja, é uma função que deve ser definida para os casos b -> a
e
c -> a
. Em outras palavras devemos definir um par de funções, o que é
compatível com a propriedade da exponenciação
\(a^{b+c} = a^b \cdot a^c\).
Uma função \((a^b)^c\) é interpretada como uma função que recebe um tipo
c
e retorna uma função de b -> a
, ou seja, uma função de alta ordem:
f :: c -> (b -> a)
Mas sabemos que essa forma é equivalente a uma função que recebe um par
(c, b)
e retorna um a
. Ou seja, \((a^b)^c = a^{(bc)}\)
Também podemos ter uma função \((a \cdot b)^c\) que é representada por:
f :: c -> (a, b)
Isso é equivalente a um par de funções c -> a
e c -> b
(nossas
funções p, q
do tipo produto) que nos dá
\((a \cdot b)^c = a^c \cdot b^c\)
Uma Categoria que possui:
Dizemos que é uma Categoria Cartesiana Fechada (CCC). Se além disso ela tiver as propriedades complementares:
Então ela é uma Categoria Bicartesiana Fechada (Bicartesian Closed Category - BCCC).
Complementando nossa tabela do isomorfismo de Curry Howard temos:
Algebra | Tipos | Lógica |
---|---|---|
\(0\) | Void |
Falso |
\(1\) | () |
Verdadeiro |
\(a + b\) | Either a b | \(a \lor b\) |
\(a * b\) | (a, b) | \(a \land b\) |
\(b^a\) | \(a \rightarrow b\) | \(a \implies b\) |
A definição de uma função é isomórfica a definição de uma implicação.
Por exemplo, nossa função eval
com assinatura:
eval :: ((a->b), a) -> b
Pode ser traduzida como: “Se b
segue de a
e a
é verdadeiro, então
b
é verdadeiro.”. Provamos essa proposição implementando, ou seja,
mostrando que esse tipo é habitável por algum valor:
eval :: ((a->b), a) -> b
eval (f, x) = f x
Vamos tentar provar a proposição \((a \lor b) \implies a\), ou seja, se \(a\) ou \(b\) forem verdadeiros, então \(a\) é verdadeiro:
f :: Either a b -> a
f (Left x) = x
f (Right y) = ???
Na segunda definição eu tenho que prover uma expressão que funcione para qualquer que seja o tipo \(a\). Impossível!
Podemos então usar funções definidas em Haskell como provas de proposições lógicas.
https://bartoszmilewski.com/2014/10/28/category-theory-for-programmers-the-preface/