O que significa coalgebra no contexto da programação?

339

Já ouvi o termo "coalgebras" várias vezes na programação funcional e nos círculos PLT, especialmente quando a discussão é sobre objetos, comonadas, lentes e outros. Pesquisando esse termo, páginas trazem descrições matemáticas dessas estruturas, que são praticamente incompreensíveis para mim. Alguém pode, por favor, explicar o que as barras de carvão significam no contexto da programação, qual é o significado delas e como elas se relacionam com objetos e comônadas?

desaparecido
fonte
21
Posso recomendar o excelente livro Patterns, de Jeremy Gibbons, no FP: patternsinfp.wordpress.com e seu artigo bastante compreensível "Calculating Functional Programs"? Ambos cobrem as barras de carvão de uma maneira bastante rigorosa (em comparação com, por exemplo, uma publicação no blog), mas também são razoavelmente independentes para alguém que conhece um pouco de Haskell.
Kristopher Micinski
2
@KristopherMicinski, muito interessante. Obrigado!
missingfaktor

Respostas:

474

Álgebras

Eu acho que o ponto de partida seria entender a idéia de uma álgebra . Esta é apenas uma generalização de estruturas algébricas como grupos, anéis, monóides e assim por diante. Na maioria das vezes, essas coisas são introduzidas em termos de conjuntos, mas como estamos entre amigos, falarei sobre os tipos de Haskell. (Mas não resisto a usar algumas letras gregas - elas fazem tudo parecer mais legal!)

Uma álgebra, então, é apenas um tipo τcom algumas funções e identidades. Essas funções recebem diferentes números de argumentos do tipo τe produzem a τ: uncurried, todas elas se parecem (τ, τ,…, τ) → τ. Eles também podem ter "identidades" - elementos τque têm um comportamento especial com algumas das funções.

O exemplo mais simples disso é o monóide. Um monóide é qualquer tipo τcom uma função mappend ∷ (τ, τ) → τe uma identidade mzero ∷ τ. Outros exemplos incluem coisas como grupos (que são como monóides, exceto com uma invert ∷ τ → τfunção extra ), anéis, treliças e assim por diante.

Todas as funções operam, τmas podem ter diferentes áreas. Podemos escrever isso como τⁿ → τ, onde τⁿmapeia para uma tupla de n τ. Dessa forma, faz sentido pensar em identidades como τ⁰ → τonde τ⁰está apenas a tupla vazia (). Então, na verdade, podemos simplificar a idéia de uma álgebra agora: é apenas um tipo com algum número de funções.

Uma álgebra é apenas um padrão comum em matemática que foi "fatorado", assim como fazemos com o código. As pessoas notaram que um monte de coisas interessantes - os mencionados monoides, grupos, treliças e assim por diante - seguem um padrão semelhante, então eles abstraem. A vantagem de fazer isso é a mesma da programação: cria provas reutilizáveis ​​e facilita certos tipos de raciocínio.

Álgebras F

No entanto, ainda não terminamos o fatoramento. Até agora, temos várias funções τⁿ → τ. Na verdade, podemos fazer um truque para combiná-los em uma única função. Em particular, vejamos os monoides: temos mappend ∷ (τ, τ) → τe mempty ∷ () → τ. Podemos transformá-los em uma única função usando um tipo de soma— Either. Seria assim:

op  Monoid τ  Either (τ, τ) ()  τ
op (Left (a, b)) = mappend (a, b)
op (Right ())    = mempty

Podemos realmente usar essa transformação repetidamente para combinar todas as τⁿ → τfunções em uma única, para qualquer álgebra. (Na verdade, podemos fazer isso por qualquer número de funções a → τ, b → τe assim por diante para qualquer a, b,… .)

Isso nos permite falar sobre álgebras como um tipo τcom uma única função, de alguma confusão de Eithers para um único τ. Para monoids, esta confusão é: Either (τ, τ) (); para grupos (que têm um extra de τ → τoperação), é: Either (Either (τ, τ) τ) (). É um tipo diferente para cada estrutura diferente. Então, o que todos esses tipos têm em comum? O mais óbvio é que todos são apenas somas de produtos - tipos de dados algébricos. Por exemplo, para monóides, podemos criar um tipo de argumento monóide que funcione para qualquer monóide τ:

data MonoidArgument τ = Mappend τ τ -- here τ τ is the same as (τ, τ)
                      | Mempty      -- here we can just leave the () out

Podemos fazer o mesmo com grupos, anéis, treliças e todas as outras estruturas possíveis.

O que mais há de especial em todos esses tipos? Bem, são todos Functors! Por exemplo:

instance Functor MonoidArgument where
  fmap f (Mappend τ τ) = Mappend (f τ) (f τ)
  fmap f Mempty        = Mempty

Assim, podemos generalizar ainda mais a nossa ideia de álgebra. É apenas algum tipo τcom uma função f τ → τpara algum functor f. De fato, poderíamos escrever isso como uma classe:

class Functor f  Algebra f τ where
  op  f τ  τ

Isso geralmente é chamado de "álgebra F" porque é determinado pelo functor F. Se pudéssemos aplicar parcialmente classes de tipos, poderíamos definir algo como class Monoid = Algebra MonoidArgument.

Coalgebras

Agora, espero que você tenha uma boa noção do que é uma álgebra e como é apenas uma generalização de estruturas algébricas normais. Então, o que é um F-coalgebra? Bem, o co implica que é o "dual" de uma álgebra - isto é, pegamos uma álgebra e lançamos algumas flechas. Eu vejo apenas uma seta na definição acima, então eu vou virar isso:

class Functor f  CoAlgebra f τ where
  coop  τ  f τ

E é só isso! Agora, essa conclusão pode parecer um pouco irreverente (heh). Ele mostra o que é uma coalgebra, mas na verdade não fornece nenhuma ideia de como é útil ou por que nos importamos. Eu chegarei a isso daqui a pouco, quando encontrar ou apresentar um bom exemplo ou dois: P.

Classes e Objetos

Depois de ler um pouco, acho que tenho uma boa idéia de como usar barras de carvão para representar classes e objetos. Temos um tipo Cque contém todos os possíveis estados internos de objetos na classe; a classe em si é uma coalgebra sobre a Cqual especifica os métodos e propriedades dos objetos.

Conforme mostrado no exemplo de álgebra, se tivermos várias funções como a → τe b → τpara qualquer a, b,…, podemos combiná-las em uma única função usando Eitherum tipo de soma. A "noção" dupla combinaria várias funções do tipo τ → a, τ → be assim por diante. Podemos fazer isso usando o duplo de um tipo de soma - um tipo de produto. Portanto, dadas as duas funções acima (chamadas fe g), podemos criar uma única assim:

both  τ  (a, b)
both x = (f x, g x)

O tipo (a, a)é um functor da maneira direta, por isso certamente se encaixa com a nossa noção de um F-coalgebra. Esse truque em particular nos permite agrupar várias funções diferentes - ou, para OOP, métodos - em uma única função do tipo τ → f τ.

Os elementos do nosso tipo Crepresentam o estado interno do objeto. Se o objeto tiver algumas propriedades legíveis, ele deverá poder depender do estado. A maneira mais óbvia de fazer isso é torná-las uma função C. Portanto, se queremos uma propriedade length (por exemplo object.length), teríamos uma função C → Int.

Queremos métodos que possam levar um argumento e modificar o estado. Para fazer isso, precisamos pegar todos os argumentos e produzir um novo C. Vamos imaginar um setPositionmétodo que leva uma xe uma yde coordenadas: object.setPosition(1, 2). Ele ficaria assim: C → ((Int, Int) → C).

O padrão importante aqui é que os "métodos" e "propriedades" do objeto tomam o próprio objeto como seu primeiro argumento. É exatamente como o selfparâmetro em Python e como implícito thisem muitas outras linguagens. A coálgebra essencialmente apenas encapsula o comportamento de tomar um selfparâmetro: é isso que o primeiro Cem C → F Cse.

Então, vamos juntar tudo. Vamos imaginar uma classe com uma positionpropriedade, uma namepropriedade e setPositionfunção:

class C
  private
    x, y  : Int
    _name : String
  public
    name        : String
    position    : (Int, Int)
    setPosition : (Int, Int)  C

Precisamos de duas partes para representar esta classe. Primeiro, precisamos representar o estado interno do objeto; neste caso, apenas contém dois se Intum String. (Esse é o nosso tipo C.) Então precisamos criar a coalgebra que representa a classe.

data C = Obj { x, y   Int
             , _name  String }

Temos duas propriedades para escrever. Eles são bastante triviais:

position  C  (Int, Int)
position self = (x self, y self)

name  C  String
name self = _name self

Agora só precisamos ser capazes de atualizar a posição:

setPosition  C  (Int, Int)  C
setPosition self (newX, newY) = self { x = newX, y = newY }

É como uma classe Python com suas selfvariáveis explícitas . Agora que temos várias self →funções, precisamos combiná-las em uma única função para a coalgebra. Podemos fazer isso com uma tupla simples:

coop  C  ((Int, Int), String, (Int, Int)  C)
coop self = (position self, name self, setPosition self)

O tipo ((Int, Int), String, (Int, Int) → c)-por qualquer c -é um functor, por isso cooptem a forma que queremos: Functor f ⇒ C → f C.

Diante disso, Cjuntamente com a coopforma de uma coalgebra, especifica a classe que dei acima. Você pode ver como podemos usar essa mesma técnica para especificar qualquer número de métodos e propriedades para nossos objetos.

Isso nos permite usar o raciocínio coalgéico para lidar com as classes. Por exemplo, podemos introduzir a noção de um "homomorfismo F-coalgebra" para representar transformações entre classes. Este é um termo assustador que significa apenas uma transformação entre barras de carvão que preserva a estrutura. Isso torna muito mais fácil pensar em mapear classes para outras classes.

Em resumo, um F-coalgebra representa uma classe por possuir várias propriedades e métodos que dependem de um selfparâmetro que contém o estado interno de cada objeto.

Outras categorias

Até agora, falamos sobre álgebras e coalgebras como tipos Haskell. Uma álgebra é apenas um tipo τcom uma função f τ → τe uma coalgebra é apenas um tipo τcom uma função τ → f τ.

No entanto, nada realmente vincula essas idéias a Haskell em si . De fato, eles geralmente são introduzidos em termos de conjuntos e funções matemáticas, em vez de tipos e funções Haskell. De fato, podemos generalizar esses conceitos para qualquer categoria!

Podemos definir uma álgebra F para alguma categoria C. Primeiro, precisamos de um functor F : C → C- ou seja, um endofunctor . (All Haskell Functors são realmente endofunctors de Hask → Hask.) Então, uma álgebra é apenas um objeto Aa partir de Cuma morphism F A → A. Uma coalgebra é a mesma, exceto com A → F A.

O que ganhamos ao considerar outras categorias? Bem, podemos usar as mesmas idéias em diferentes contextos. Como mônadas. Em Haskell, uma mônada é algum tipo M ∷ ★ → ★com três operações:

map         β)  (M α  M β)
return    α  M α
join      M (M α)  M α

A mapfunção é apenas uma prova do fato de que Mé a Functor. Então, podemos dizer que uma mônada é apenas um functor com duas operações: returne join.

Os próprios fundores formam uma categoria, com os morfismos entre eles sendo chamados de "transformações naturais". Uma transformação natural é apenas uma maneira de transformar um functor em outro, preservando sua estrutura. Aqui está um bom artigo que ajuda a explicar a ideia. Ele fala sobre concat, que é apenas joinpara listas.

Com os functores Haskell, a composição de dois functores é um próprio functor. No pseudocódigo, poderíamos escrever o seguinte:

instance (Functor f, Functor g)  Functor (f  g) where
  fmap fun x = fmap (fmap fun) x

Isso nos ajuda a pensar joincomo um mapeamento f ∘ f → f. O tipo de joiné ∀α. f (f α) → f α. Intuitivamente, podemos ver como uma função válida para todos os tipos αpode ser pensada como uma transformação de f.

returné uma transformação semelhante. Seu tipo é ∀α. α → f α. Isso parece diferente - o primeiro αnão está "em" um functor! Felizmente, nós podemos corrigir isso adicionando um functor identidade aí: ∀α. Identity α → f α. Então returné uma transformação Identity → f.

Agora podemos pensar em uma mônada como apenas uma álgebra baseada em algum functor fcom operações f ∘ f → fe Identity → f. Isso não parece familiar? É muito parecido com um monóide, que era apenas algum tipo τcom operações τ × τ → τe () → τ.

Portanto, uma mônada é como um monóide, exceto que, em vez de ter um tipo, temos um functor. É o mesmo tipo de álgebra, apenas em uma categoria diferente. (É aqui que a frase "Uma mônada é apenas um monóide na categoria de endofuncores" vem até onde eu sei.)

Agora, temos essas duas operações: f ∘ f → fe Identity → f. Para obter a correspondente coalgebra, basta girar as setas. Isso nos dá duas novas operações: f → f ∘ fe f → Identity. Podemos transformá-los em tipos Haskell adicionando variáveis ​​de tipo como acima, fornecendo-nos ∀α. f α → f (f α)e ∀α. f α → α. Isso se parece com a definição de uma comonada:

class Functor f  Comonad f where
  coreturn  f α  α
  cojoin    f α  f (f α)

Então uma comonada é então uma coalgebra em uma categoria de endofuncionadores.

Tikhon Jelvis
fonte
45
Isso é incrivelmente valioso. Eu consegui vagar algumas intuições sobre todo esse negócio de álgebra-F com base em leituras e exemplos (por exemplo, ao ver seu uso com catamofisismos), mas isso é tudo absolutamente claro até para mim. Obrigado!
Luis Casillas
28
Esta é uma ótima explicação.
Edward KMETT 16/04
5
@ EdwardKmett: Obrigado. As coisas que adicionei sobre classes e objetos estão bem? Eu só li sobre isso hoje, mas parece fazer sentido.
Tikhon Jelvis
7
Pelo que vale a pena: A "categoria de endofunitores" aqui é mais precisamente uma categoria cujos objetos são endofuncionais em alguma categoria e cujas flechas são transformações naturais. Esta é uma categoria monoidal, com a composição de functor correspondente a (,)e o functor de identidade a (). Um objeto monóide dentro de uma categoria monoidal é um objeto com setas correspondentes à sua álgebra monóide, que descreve um objeto monóide em Hask com tipos de produtos como a estrutura monoidal. Um objeto monóide na categoria de endofunitores em C é uma mônada em C; portanto, sim, seu entendimento está correto. :]
CA McCann
8
Esse foi um final épico!
precisa saber é o seguinte
85

Álgebras-F e barras-carvão são estruturas matemáticas que são instrumentais no raciocínio sobre tipos indutivos (ou recursivos ).

Álgebras F

Vamos começar primeiro com álgebras F. Vou tentar ser o mais simples possível.

Eu acho que você sabe o que é um tipo recursivo. Por exemplo, este é um tipo para uma lista de números inteiros:

data IntList = Nil | Cons (Int, IntList)

É óbvio que é recursivo - de fato, sua definição se refere a si mesma. Sua definição consiste em dois construtores de dados, que têm os seguintes tipos:

Nil  :: () -> IntList
Cons :: (Int, IntList) -> IntList

Note que eu escrevi o tipo de Nilas () -> IntList, não simplesmente IntList. De fato, são tipos equivalentes do ponto de vista teórico, porque o ()tipo tem apenas um habitante.

Se escrevermos assinaturas dessas funções de uma maneira mais teórica, obteremos

Nil  :: 1 -> IntList
Cons :: Int × IntList -> IntList

onde 1é um conjunto de unidades (conjunto com um elemento) e A × Boperação é um produto cruzado de dois conjuntos Ae B(ou seja, conjunto de pares (a, b)onde apassa por todos os elementos de Ae bpassa por todos os elementos de B).

União disjunta de dois conjuntos Ae Bé um conjunto A | Bque é uma união de conjuntos {(a, 1) : a in A}e {(b, 2) : b in B}. Essencialmente, é um conjunto de todos os elementos de ambos Ae B, mas com cada um desses elementos 'marcados' como pertencentes a um Aou outro B, portanto, quando escolhermos qualquer elemento, A | Bsaberemos imediatamente se esse elemento veio Aou não B.

Podemos 'juntar-se' Nile Consfunções, para que eles formem uma única função trabalhando em um conjunto 1 | (Int × IntList):

Nil|Cons :: 1 | (Int × IntList) -> IntList

De fato, se a Nil|Consfunção é aplicada ao ()valor (que obviamente pertence ao 1 | (Int × IntList)conjunto), ela se comporta como se fosse Nil; se Nil|Consfor aplicado a qualquer valor do tipo (Int, IntList)(esses valores também estão no conjunto 1 | (Int × IntList), ele se comporta como Cons.

Agora considere outro tipo de dados:

data IntTree = Leaf Int | Branch (IntTree, IntTree)

Possui os seguintes construtores:

Leaf   :: Int -> IntTree
Branch :: (IntTree, IntTree) -> IntTree

que também pode ser associado a uma função:

Leaf|Branch :: Int | (IntTree × IntTree) -> IntTree

Pode-se ver que essas duas joinedfunções têm um tipo semelhante: ambas se parecem

f :: F T -> T

onde Fé um tipo de transformação que leva o nosso tipo e dá tipo mais complexo, que consiste em xe |operações, usos Te possivelmente outros tipos. Por exemplo, para IntListe IntTree Ftem a seguinte aparência:

F1 T = 1 | (Int × T)
F2 T = Int | (T × T)

Podemos notar imediatamente que qualquer tipo algébrico pode ser escrito dessa maneira. De fato, é por isso que eles são chamados de 'algébricos': eles consistem em um número de 'somas' (uniões) e 'produtos' (produtos cruzados) de outros tipos.

Agora podemos definir álgebra F. A álgebra F é apenas um par (T, f), onde Té algum tipo e fé uma função do tipo f :: F T -> T. Nos nossos exemplos, as álgebras F são (IntList, Nil|Cons)e (IntTree, Leaf|Branch). Observe, no entanto, que, apesar desse tipo de ffunção, é o mesmo para cada F, Te felas próprias podem ser arbitrárias. Por exemplo, (String, g :: 1 | (Int x String) -> String)ou (Double, h :: Int | (Double, Double) -> Double)para alguns, ge htambém são álgebras F para F. correspondente

Posteriormente, podemos introduzir homomorfismos da álgebra F e, em seguida , álgebras F iniciais , que possuem propriedades muito úteis. De fato, (IntList, Nil|Cons)é uma álgebra F1 inicial e (IntTree, Leaf|Branch)é uma álgebra F2 inicial. Não apresentarei definições exatas desses termos e propriedades, pois são mais complexos e abstratos do que o necessário.

No entanto, o fato de, digamos, (IntList, Nil|Cons)ser álgebra F, permite definir foldfunções semelhantes a esse tipo. Como você sabe, fold é um tipo de operação que transforma algum tipo de dados recursivo em um valor finito. Por exemplo, podemos dobrar uma lista de números inteiros em um único valor, que é uma soma de todos os elementos da lista:

foldr (+) 0 [1, 2, 3, 4] -> 1 + 2 + 3 + 4 = 10

É possível generalizar essa operação em qualquer tipo de dados recursivo.

A seguir, é uma assinatura da foldrfunção:

foldr :: ((a -> b -> b), b) -> [a] -> b

Observe que usei chaves para separar os dois primeiros argumentos do último. Essa não é uma foldrfunção real , mas é isomórfica (ou seja, você pode facilmente obter uma da outra e vice-versa). Parcialmente aplicado foldrterá a seguinte assinatura:

foldr ((+), 0) :: [Int] -> Int

Podemos ver que essa é uma função que pega uma lista de números inteiros e retorna um único número inteiro. Vamos definir essa função em termos do nosso IntListtipo.

sumFold :: IntList -> Int
sumFold Nil         = 0
sumFold (Cons x xs) = x + sumFold xs

Vemos que essa função consiste em duas partes: a primeira parte define o comportamento dessa função em Nilparte IntListe a segunda parte define o comportamento da função em Consparte.

Agora, suponha que não estamos programando em Haskell, mas em alguma linguagem que permita o uso de tipos algébricos diretamente nas assinaturas de tipo (bem, tecnicamente, o Haskell permite o uso de tipos algébricos por tuplas e Either a btipos de dados, mas isso levará a verbosidade desnecessária). Considere uma função:

reductor :: () | (Int × Int) -> Int
reductor ()     = 0
reductor (x, s) = x + s

Pode-se ver que reductoré uma função do tipo F1 Int -> Int, assim como na definição da álgebra F! De fato, o par (Int, reductor)é uma álgebra F1.

Como IntListé uma álgebra F1 inicial, para cada tipo Te função r :: F1 T -> Texiste uma função chamada catamorfismo para r, que se converte IntListem T, e essa função é única. De fato, em nosso exemplo um catamorphism para reductoré sumFold. Observe como reductore sumFoldsão semelhantes: eles têm quase a mesma estrutura! Na reductordefinição, o suso do parâmetro (do tipo corresponde a T) corresponde ao uso do resultado da computação de sumFold xsna sumFolddefinição.

Apenas para torná-lo mais claro e ajudá-lo a ver o padrão, aqui está outro exemplo, e começamos novamente a partir da função de dobra resultante. Considere a appendfunção que anexa seu primeiro argumento ao segundo:

(append [4, 5, 6]) [1, 2, 3] = (foldr (:) [4, 5, 6]) [1, 2, 3] -> [1, 2, 3, 4, 5, 6]

É assim que fica no nosso IntList:

appendFold :: IntList -> IntList -> IntList
appendFold ys ()          = ys
appendFold ys (Cons x xs) = x : appendFold ys xs

Novamente, vamos tentar escrever o redutor:

appendReductor :: IntList -> () | (Int × IntList) -> IntList
appendReductor ys ()      = ys
appendReductor ys (x, rs) = x : rs

appendFoldé um catamorphism para appendReductorque transforma IntListem IntList.

Portanto, essencialmente, as álgebras F permitem definir 'dobras' em estruturas de dados recursivas, ou seja, operações que reduzem nossas estruturas a algum valor.

F-coalgebras

F-coalgebras são os chamados termos 'duplos' para álgebras F. Eles nos permitem definir unfoldstipos de dados recursivos, ou seja, uma maneira de construir estruturas recursivas a partir de algum valor.

Suponha que você tenha o seguinte tipo:

data IntStream = Cons (Int, IntStream)

Este é um fluxo infinito de números inteiros. Seu único construtor tem o seguinte tipo:

Cons :: (Int, IntStream) -> IntStream

Ou, em termos de conjuntos

Cons :: Int × IntStream -> IntStream

O Haskell permite padronizar a correspondência nos construtores de dados, para que você possa definir as seguintes funções trabalhando em IntStreams:

head :: IntStream -> Int
head (Cons (x, xs)) = x

tail :: IntStream -> IntStream
tail (Cons (x, xs)) = xs

Naturalmente, você pode 'unir' essas funções em uma única função do tipo IntStream -> Int × IntStream:

head&tail :: IntStream -> Int × IntStream
head&tail (Cons (x, xs)) = (x, xs)

Observe como o resultado da função coincide com a representação algébrica do nosso IntStreamtipo. O mesmo pode ser feito para outros tipos de dados recursivos. Talvez você já tenha notado o padrão. Estou me referindo a uma família de funções do tipo

g :: T -> F T

Onde Testá algum tipo. A partir de agora vamos definir

F1 T = Int × T

Agora, F-coalgebra é um par (T, g), onde Té um tipo e gé uma função do tipo g :: T -> F T. Por exemplo, (IntStream, head&tail)é um F1-coalgebra. Novamente, assim como nas álgebras F, ge Tpode ser arbitrário, por exemplo, (String, h :: String -> Int x String)também é uma álgebra F1 por alguns h.

Entre todas as barras de carvão F, existem as chamadas barras de terminal F , que são duplas às álgebras F iniciais. Por exemplo, IntStreamé um terminal F-coalgebra. Isso significa que, para todo tipo Te função p :: T -> F1 T, existe uma função chamada anamorfismo , que se converte Tem IntStream, e essa função é única.

Considere a seguinte função, que gera um fluxo de números inteiros sucessivos a partir do dado:

nats :: Int -> IntStream
nats n = Cons (n, nats (n+1))

Agora vamos inspecionar uma função natsBuilder :: Int -> F1 Int, ou seja natsBuilder :: Int -> Int × Int:

natsBuilder :: Int -> Int × Int
natsBuilder n = (n, n+1)

Novamente, podemos ver alguma semelhança entre natse natsBuilder. É muito semelhante à conexão que observamos anteriormente com redutores e dobras. natsé um anamorfismo para natsBuilder.

Outro exemplo, uma função que pega um valor e uma função e retorna um fluxo de aplicativos sucessivos da função para o valor:

iterate :: (Int -> Int) -> Int -> IntStream
iterate f n = Cons (n, iterate f (f n))

Sua função de construtor é a seguinte:

iterateBuilder :: (Int -> Int) -> Int -> Int × Int
iterateBuilder f n = (n, f n)

Então iterateé um anamorfismo para iterateBuilder.

Conclusão

Então, em resumo, as álgebras-F permitem definir dobras, isto é, operações que reduzem a estrutura recursiva em um único valor, e as álgebras-F permitem fazer o oposto: construir uma estrutura [potencialmente infinita] a partir de um único valor.

De fato, as álgebras F e as coalgebras de Haskell coincidem. Esta é uma propriedade muito agradável, que é uma conseqüência da presença do valor 'bottom' em cada tipo. Assim, em Haskell, podem ser criadas dobras e desdobramentos para todos os tipos recursivos. No entanto, o modelo teórico por trás disso é mais complexo do que o que apresentei acima, por isso evitei-o deliberadamente.

Espero que isto ajude.

Vladimir Matveev
fonte
O tipo e a definição de appendReductoraparência um pouco estranha e realmente não me ajudaram a ver o padrão lá ... :) Você pode verificar se está correto? .. Como devem ser os tipos redutores em geral? Na definição de rlá, é F1determinado pelo IntList, ou é um F arbitrário?
precisa saber é o seguinte
37

Percorrendo o artigo tutorial Um tutorial sobre (co) álgebras e (co) indução deve fornecer algumas dicas sobre co-álgebra em ciência da computação.

Abaixo está uma citação para convencê-lo,

Em termos gerais, um programa em alguma linguagem de programação manipula dados. Durante o desenvolvimento da ciência da computação nas últimas décadas, ficou claro que uma descrição abstrata desses dados é desejável, por exemplo, para garantir que o programa de alguém não dependa da representação específica dos dados nos quais opera. Além disso, essa abstração facilita as provas de correção.
Esse desejo levou ao uso de métodos algébricos em ciência da computação, em um ramo chamado especificação algébrica ou teoria abstrata de tipos de dados. O objeto de estudo são os tipos de dados em si mesmos, usando noções de técnicas conhecidas da álgebra. Os tipos de dados usados ​​pelos cientistas da computação geralmente são gerados a partir de uma determinada coleção de operações (construtoras) e é por esse motivo que a "inicial" das álgebras desempenha um papel tão importante.
Técnicas algébricas padrão provaram ser úteis na captura de vários aspectos essenciais das estruturas de dados usadas na ciência da computação. Mas acabou sendo difícil descrever algebricamente algumas das estruturas inerentemente dinâmicas que ocorrem na computação. Tais estruturas geralmente envolvem uma noção de estado, que pode ser transformada de várias maneiras. As abordagens formais para esses sistemas dinâmicos baseados no estado geralmente fazem uso de autômatos ou sistemas de transição, como referências iniciais clássicas.
Durante a última década, cresceu gradualmente a percepção de que esses sistemas baseados no estado não deveriam ser descritos como álgebras, mas como as chamadas co-álgebras. Estas são as duplas formais de álgebras, de uma maneira que será precisa neste tutorial. A propriedade dupla de "inicial" para álgebras, ou seja, finalidade, foi crucial para essas co-álgebras. E o princípio do raciocínio lógico necessário para essas co-álgebras finais não é indução, mas co-indução.


Prelúdio, sobre a teoria das categorias. A teoria das categorias deve renomear a teoria dos functores. Como categorias são o que é preciso definir para definir functores. (Além disso, é necessário definir functores para definir transformações naturais.)

O que é um functor? É uma transformação de um conjunto para outro que preserva sua estrutura. (Para mais detalhes, há muitas boas descrições na rede).

O que é uma álgebra F? É a álgebra do functor. É apenas o estudo da propriedade universal do functor.

Como isso pode ser vinculado à ciência da computação? O programa pode ser visto como um conjunto estruturado de informações. A execução do programa corresponde à modificação desse conjunto estruturado de informações. Parece bom que a execução preserve a estrutura do programa. Então a execução pode ser vista como a aplicação de um functor sobre esse conjunto de informações. (Aquele que define o programa).

Por que F-co-álgebra? Os programas são duplos por essência, pois são descritos por informações e agem sobre ela. Então, principalmente as informações que compõem o programa e as modificam podem ser vistas de duas maneiras.

  • Dados que podem ser definidos como as informações que estão sendo processadas pelo programa.
  • Estado que pode ser definido como a informação que está sendo compartilhada pelo programa.

Então, nesta fase, eu gostaria de dizer isso,

  • A álgebra F é o estudo da transformação funcional atuando sobre o universo de dados (conforme definido aqui).
  • F-co-álgebras é o estudo da transformação funcional atuando no Universo do Estado (como aqui definido).

Durante a vida de um programa, dados e estado coexistem, e eles se completam. Eles são duplos.

zurgl
fonte
5

Começarei com coisas obviamente relacionadas à programação e depois adicionarei algumas coisas de matemática, para mantê-las tão concretas e práticas quanto possível.


Vamos citar alguns cientistas da computação sobre coindução ...

http://www.cs.umd.edu/~micinski/posts/2012-09-04-on-understanding-coinduction.html

Indução é sobre dados finitos, co-indução é sobre dados infinitos.

O exemplo típico de dados infinitos é o tipo de uma lista lenta (um fluxo). Por exemplo, digamos que temos o seguinte objeto na memória:

 let (pi : int list) = (* some function which computes the digits of
 π. *)

O computador não pode conter todos os π, porque possui apenas uma quantidade finita de memória! Mas o que ele pode fazer é manter um programa finito, que produzirá qualquer expansão arbitrariamente longa de π que você deseja. Contanto que você use apenas partes finitas da lista, poderá calcular com essa lista infinita quantas vezes precisar.

No entanto, considere o seguinte programa:

let print_third_element (k : int list) =   match k with
     | _ :: _ :: thd :: tl -> print thd


 print_third_element pi

Este programa deve imprimir o terceiro dígito de pi. Mas em algumas linguagens, qualquer argumento para uma função é avaliado antes de ser passado para uma função (avaliação estrita, não preguiçosa). Se usarmos essa ordem de redução, nosso programa acima será executado para sempre computando os dígitos de pi antes que ele possa ser passado para a função de impressora (o que nunca acontece). Como a máquina não possui memória infinita, o programa acabará ficando sem memória e trava. Essa pode não ser a melhor ordem de avaliação.

http://adam.chlipala.net/cpdt/html/Coinductive.html

Em linguagens de programação preguiçosas, como Haskell, existem infinitas estruturas de dados em toda parte. Listas infinitas e tipos de dados mais exóticos fornecem abstrações convenientes para a comunicação entre partes de um programa. Alcançar conveniência semelhante sem estruturas preguiçosas infinitas exigiria, em muitos casos, inversões acrobáticas do fluxo de controle.

http://www.alexandrasilva.org/#/talks.html exemplos de barras de carvão por Alexandra Silva


Relacionando o Contexto Matemático Ambiental às Tarefas de Programação Habituais

O que é "uma álgebra"?

As estruturas algébricas geralmente se parecem com:

  1. Coisa
  2. O que as coisas podem fazer

Isso deve soar como objetos com 1. propriedades e 2. métodos. Ou melhor ainda, deve parecer assinaturas de tipo.

Os exemplos matemáticos padrão incluem ⊃ grupo ⊃ espaço vetorial oid "uma álgebra". Monoides são como autômatos: sequências de verbos (por exemplo, f.g.h.h.nothing.f.g.f). Um gitlog que sempre adiciona histórico e nunca o exclui seria um monóide, mas não um grupo. Se você adiciona inversas (por exemplo, números negativos, frações, raízes, exclusão do histórico acumulado, quebra de um espelho quebrado), obtém um grupo.

Grupos contêm itens que podem ser adicionados ou subtraídos juntos. Por exemplo, Durations podem ser adicionados juntos. (Mas Dates não podem.) As durações residem em um espaço vetorial (não apenas em um grupo) porque também podem ser dimensionadas por números externos. (Uma assinatura de tipo de scaling :: (Number,Duration) → Duration.)

Os espaços de vetor de álgebras podem fazer outra coisa: existem alguns m :: (T,T) → T. Chame isso de "multiplicação" ou não, porque uma vez que você sai Integers, é menos óbvio o que "multiplicação" (ou "exponenciação" ) deve ser.

(É por isso que as pessoas buscam propriedades universais (teóricas das categorias): dizer a elas como a multiplicação deve fazer ou ser :

propriedade universal do produto )


Álgebras → Coalgebras

A multiplicação é mais fácil de definir de uma maneira que parece não-arbitrária, do que a multiplicação, porque para passar, T → (T,T)basta repetir o mesmo elemento. ("mapa diagonal" - como matrizes / operadores diagonais na teoria espectral)

O aconselhamento é geralmente o rastreamento (soma das entradas diagonais), embora, novamente, o importante seja o que o seu aconselhamento faz ; traceé apenas uma boa resposta para matrizes.

A razão para olhar para um espaço duplo , em geral, é porque é mais fácil pensar nesse espaço. Por exemplo, às vezes é mais fácil pensar em um vetor normal do que no plano normal, mas você pode controlar aviões (incluindo hiperplanos) com vetores (e agora estou falando do vetor geométrico familiar, como em um traçador de raios) .


Domando (des) dados estruturados

Os matemáticos podem estar modelando algo divertido como o TQFT , enquanto os programadores precisam lutar com

  • datas / horas (+ :: (Date,Duration) → Date ),
  • lugares ( Paris(+48.8567,+2.3508) ! É uma forma, não um ponto.),
  • JSON não estruturado, que deveria ser consistente em algum sentido,
  • XML errado-mas-próximo,
  • dados GIS incrivelmente complexos que devem satisfazer um monte de relações sensíveis,
  • expressões regulares que significam algo para você, mas significam consideravelmente menos para perl.
  • CRM que deve conter todos os números de telefone do executivo e localizações das vilas, nomes de (agora ex) esposa e filhos, aniversário e todos os presentes anteriores, cada um dos quais deve satisfazer relações "óbvias" (óbvias para o cliente) que são incrivelmente difícil de codificar,
  • .....

Os cientistas da computação, ao falarem sobre barras de carvão, geralmente têm em mente operações como o produto cartesiano. Eu acredito que é isso que as pessoas querem dizer quando dizem "Álgebras são barras de carvão em Haskell". Mas, na medida em que os programadores precisam modelar tipos de dados complexos como Place, Date/Timee Customer- e fazer com que esses modelos se pareçam tanto com o mundo real (ou pelo menos com a visão do mundo real do usuário final) quanto possível - acredito que os duplos, poderia ser útil além do mundo do cenário.

isomorfismos
fonte