Catamorfismos e álgebras F
então eu ouço muitas vezes as palavras “catamorfismo”e” esquemas de recursão”. O que se passa?
Catamorfismos (ou cata) são generalizações do conceito de um fold na programação funcional. Dada uma álgebra F e uma estrutura recursiva de dados, um catamorfismo produzirá um valor ao avaliar recursivamente a sua estrutura de dados.
o que é uma álgebra F? Talvez possa mostrar alguns exemplos de código primeiro?
a configuração não é tão simples assim vamos começar simples. Digamos que você tenha a seguinte estrutura de dados para representar uma expressão:
E uma expressão simples pode ficar assim:
Tendo apenas uma expressão estrutura de dados é inútil, é claro que você vai precisar de uma função de avaliar e obter o resultado:
Este é o lugar onde catamorphism generalização vem em:
🤨, mas qual é o ponto? Estás a aplicar uma discussão a uma função?
isso é porque é notReallyCata
. A função real cata
é genérica e não depende de qualquer estrutura de dados particular ou função de Avaliador. Veja, a criação de uma estrutura de dados recursiva e então dobrando sobre ela é um padrão comum que cata
tenta generalizar.Ok, então como é o verdadeiro cata?
🤯
por isso começamos com notReallyCata
. Vamos quebrar a implementação mais tarde até que ela clica. Mas agora vamos continuar com o nosso exemplo Expression
. Primeiro, precisamos nos livrar da recursão introduzindo um parâmetro de tipo:
todas as referências a Expression
são substituídas por a
parâmetro do tipo de modo que a estrutura de dados não é mais recursiva.
por que existe um
F
no final dos construtores de tipo?Ainda bem que perguntou.Functor
:nada de Especial, apenas a aplicar alguma função ao valor embrulhado preservando a stucture.
Não sei por que temos que 🤔
não faz sentido agora, mas ele vai um pouco mais tarde. Agora, a forma como criamos a nossa expressão não se alterou (exceto para nomes construtor):
Mas o tipo resultante é diferente:
expr
cai tudo em um únicoExpression
enquantoexprF
codifica as informações sobre o nível de aninhamento de nossa árvore de expressão. Por falar em avaliação, é assim que podemos implementar eval forExpressionF
:a principal diferença com
evalExpr
original é que não temos chamada recursiva paraevalExprF
(ExpressionF
não é recursiva, lembra-se?). Isso também significa que a nossa avaliador pode trabalhar apenas com um único nível de expressão:E não vai compilar neste:
Simplesmente porque
exprF
expepectsExpressionF Int
e estamos empurrandoExpressionF (ExpressionF Int)
.Para o fazer funcionar, poderíamos definir outro avaliador.:parece um pouco ad hoc, e se tiver expressões profundamente aninhadas?
Sim, para arbitrário aninhadas expressão esta abordagem não é escalável — cada nível de aninhamento requer que você escreva função especializada.
há uma maneira de generalizar este nidificação com um novo tipo:
arranjar? Parece uma estrutura de dados recursiva que não faz muito. Como é útil?
vamos primeiro olhar para a expressão antes do sinal de igual: na verdade
Fix
é uma estrutura de dados recursiva que tem um parâmetro de tipof
. Este parâmetro tem tipo* -> *
e.g. ele também tem um parâmetro tipo. Por exemplo, você não pode construirFix
fornecendoInt
ouBool
, tem que ser algo comoMaybe
,List
ou…ExpressionF
. É por isso que introduzimos o parâmetro tipo paraExpressionF
. Em seguida, após o sinal de igual, temos um único construtor de tipoFx
tomando um único argumento de tipof (Fix f)
que é basicamente uma expressão que constrói o valor def
. No caso deMaybe
seriaMaybe (Fix Maybe)
e então a coisa toda é embrulhada comFx
em TipoFix Maybe
.a assinatura do tipo é confusa para se ler no início porque o construtor do tipo pode ter o mesmo nome que o próprio tipo mais auto-referenciação. Mas não há muito mais para ele do que apenas empacotar um tipo de ordem superior em uma estrutura de dados. Btw,
unfix
é um oposto aFx
e tudo o que faz é corresponder padrão emFx
e retorna o valor embrulhado, nada de especial.agora, vamos substituir cada
ExpressionF
da nossa árvore de expressão porFix ExpressionF
. Observe a diferença na construção de expressões com e semFx
— eles são basicamente o mesmo, exceto que precisa precederFx $
:O tipo resultante de um “fixo”, versão
Fix ExpressionF
então estamos de volta a uma representação recursiva, mas agora nós temos que usarunfix
função para obter os nossos não recursiva estrutura de dados de volta.quais são os benefícios de ter
Fix
? Parece que é a mesma abordagem do tipo originalExpression
mas agora temos este absurdo estranhoFix
eunfix
?Sim, mas estamos tentando generalizar o processo de dobramento, que requer a introdução de abstrações adicionais, como
Fix
eAlgebra
que discutiremos mais tarde. Sê paciente comigo, vai fazer mais sentido mais tarde.Então temos a nossa estrutura de dados “fixa”, como seria a função de avaliação?Dado um
Fix ExpressionF
a única coisa que podemos fazer com ele é chamada deunfix
que produzExpressionF (Fix ExpressionF)
:A devolvidos
ExpressionF
pode ser um dos nossosValueF
,AddF
ouMultF
tendo umFix ExpressionF
o tipo de parâmetro. Faz sentido para fazer a correspondência de padrão e decidir o que fazer a seguir:Sim, parece o mesmo que o nosso primeiro recursiva avaliador
Expression
com adição de ter para desembrulhar a expressãounfix
. Então, porquê preocupar-se comFix
?Aqui está a chave.: vamos reutilizar o nosso avaliador “fix-less” original paraExpressionF
e, de alguma forma, distribuí-lo pela stuctureFix ExpressionF
. Portanto, isso deve ser uma função que toma dois argumentos — o avaliador e a estrutura para avaliar:Vamos tentar descobrir a implementação — a primeira coisa lógica a fazer é usar
unfix
para obterExpressionF
e, em seguida, talvez passar paraevaluator
:Obviamente, isso não funciona,
evaluator
esperaExpressionF Int
e nãoExpressionF (Fix ExpressionF)
. A propósito, lembra-te queExpressionF
é umFunctor
? Este é o local onde fica a calhar podemos usarfmap
para aplicar o mesmo processo para o nível interno de nossa árvore de expressão:Tome um momento e pense sobre o que acontece: estamos passando uma função recursiva em
fmap
. Se a expressão atual forAddF
ouMultF
então esta função será passada um nível mais profundo efmap
será chamada novamente. Isto irá acontecer até chegarmos aValueF
, o fmapping sobreValueF
devolve o valor do tipoExpressionF Int
e é exactamente isso que a nossa funçãoevaluator
aceita.observando
almostCata
podemos ver que ele realmente não tem nada específico paraExpressionF
ouInt
tipo e, teoricamente, pode ser generalizado, com algum tipo de parâmetrof
. A única restrição deve ser ter uma instânciaFunctor
paraf
, porque estamos usandofmap
:e esta é a versão final de
cata
. Aqui está a implementação completa com alguns exemplos de uso:acho que isso é fixe. Mas porquê?
muitos conceitos na teoria das categorias e na programação funcional são bastante abstractos e, por vezes, é difícil encontrar aplicação prática imediata para uma determinada ideia. Mas procurar abstrações e generalizações é útil para encontrar padrões e soluções elegantes para problemas que de outra forma requerem implementação ad-hoc.A propósito, ao generalizar a nossa função
ExpressionF -> Int
aFunctor f => (f a -> a)
descobrimos outro conceito importante chamado F-Álgebra. Basicamente F-álgebra é um triplo de functorf
, algum tipoa
e função Avaliadoraf a -> a
. Note quea
aqui não polimórfico — tem que ser um tipo concreto, comoInt
ouBool
e é chamado de tipo portador. Para qualquer endo-functorf
você pode criar vários F-Álgebra baseados nele. Tomemos como exemplo as nossas expressões — endo-functorf
éExpressionF
,a
éInt
e o avaliador éevalExprF
. Mas podemos mudar o transportador tipo e produzir mais álgebras:isso é apenas diferentes avaliadores que podem ser passados para
cata
, certo?Sim, estamos escolhendo diferentes tipos de portadoras e escolhendo a nossa implementação. Mas aí está o truque-há uma mãe de todos os avaliadores que podemos criar escolhendo o nosso tipo de porta-aviões para ser …
Fix ExprF
.avaliar a
Int
ouBool
faz todo o sentido, mas o que é que esteinitialAlgebra
avaliaria? Quando preciso de terFix
de algo como resultado do meu avaliador?é claro que você não vai escrever algo assim você mesmo, só quer mostrar-lhe o significado mais profundo por trás das álgebras f e cata. Na verdade, já temos uma implementação para esse avaliador e isso é exatamente
Fx
construtor:espera, o
Fx
é um avaliador? Isso é uma loucura.Yes and it does the most simple thing you can do-save the expession into a data structure. Enquanto todos os outros avaliadores (
algebra0
,algebra1
) produziu alguns de valor, reduzindo a expressão (como fazer soma ou concatenação)Fx
apenas envolve a expressão, sem perder quaisquer dados.é por Isso que introduzimos
Fix
em primeiro lugar — você o primeiro a avaliar sua estrutura de dados original comFx
na inicial álgebraFix f
e, em seguida, usandocata
o ‘real’ avaliação acontece porfmap
ing seu concreto avaliador sobre o primeiro álgebra.do ponto de vista da teoria das categorias, Todas as álgebras baseadas no mesmo functor de endo formam uma categoria. Esta categoria tem um objeto inicial que é a nossa álgebra inicial criada escolhendo o tipo portador como
Fix f
. Há alguns posts grandes blog por Bartosz Milewski que eu recomendo verificar para fora se você quiser obter compreensão categórica profunda.ainda É muito difícil de compreender, eu não acho que eu entender completamente o conceito
É sempre melhor fazer as mãos sobre: tente re-implementação
Fix
ecata
em seu próprio país, pensar sobre possíveis estruturas de dados e álgebra. Por exemplo, umString
pode ser representado recursivamente (como umChar
cabeça e cauda deString
), olength
de uma cadeia pode ser calculado comcata
. Aqui estão alguns grandes recursos para mais leitura:
- Entendimento F-Álgebras e ligeiramente diferente do F-Álgebras por Bartosz Milewski
- Catamorphisms em 15 minutos por Chris Jones
- Puro, Funcional de Programação de Banco de dados com ponto fixo Tipos de Rob Norris
- Catamorphisms em Haskell wiki
- Prático recursão esquemas de Jared (Tobin