Existem alguns problemas que são facilmente resolvidos por tipos de dados algébricos, por exemplo, um tipo de lista pode ser expresso de maneira muito sucinta como:
data ConsList a = Empty | ConsCell a (ConsList a)
consmap f Empty = Empty
consmap f (ConsCell a b) = ConsCell (f a) (consmap f b)
l = ConsCell 1 (ConsCell 2 (ConsCell 3 Empty))
consmap (+1) l
Este exemplo específico está em Haskell, mas seria semelhante em outros idiomas com suporte nativo para Tipos de Dados Algébricos.
Acontece que existe um mapeamento óbvio para a subtipo no estilo OO: o tipo de dados se torna uma classe base abstrata e todo construtor de dados se torna uma subclasse concreta. Aqui está um exemplo no Scala:
sealed abstract class ConsList[+T] {
def map[U](f: T => U): ConsList[U]
}
object Empty extends ConsList[Nothing] {
override def map[U](f: Nothing => U) = this
}
final class ConsCell[T](first: T, rest: ConsList[T]) extends ConsList[T] {
override def map[U](f: T => U) = new ConsCell(f(first), rest.map(f))
}
val l = (new ConsCell(1, new ConsCell(2, new ConsCell(3, Empty)))
l.map(1+)
A única coisa necessária além da subclasse ingênua é uma maneira de selar classes, ou seja, uma maneira de tornar impossível adicionar subclasses a uma hierarquia.
Como você abordaria esse problema em uma linguagem como C # ou Java? Os dois obstáculos que encontrei ao tentar usar tipos de dados algébricos em c # foram:
- Não consegui descobrir como é chamado o tipo inferior em C # (ou seja, não consegui descobrir em que colocar
class Empty : ConsList< ??? >
) - Não consegui descobrir uma maneira de selar
ConsList
para que nenhuma subclasse possa ser adicionada à hierarquia
Qual seria a maneira mais idiomática de implementar tipos de dados algébricos em C # e / ou Java? Ou, se não for possível, qual seria a substituição idiomática?
Respostas:
Existe uma maneira fácil, porém pesada, de selar classes em Java. Você coloca um construtor privado na classe base e cria subclasses com classes internas.
Adote um padrão de visitante para envio.
Meu projeto jADT: Java Algebraic DataTypes gera todo esse padrão para você https://github.com/JamesIry/jADT
fonte
Either
. Veja minha perguntaVocê pode conseguir isso usando o padrão de visitante , que complementará a correspondência de padrões. Por exemplo
pode ser escrito em Java como
A vedação é alcançada pela
Visitor
classe. Cada um de seus métodos declara como desconstruir uma das subclasses. Você poderia adicionar mais subclasses, mas isso teria que ser implementadoaccept
e chamado um dosvisit...
métodos, para que ele tivesse que se comportar comoCons
ou gostarNil
.fonte
Se você abusar dos parâmetros nomeados do C # (introduzidos no C # 4.0), poderá criar tipos de dados algébricos fáceis de combinar:
Aqui está a implementação da
Either
classe:fonte
class Right<R> : Either<Bot,R>
:, onde Either é alterado para uma interface com parâmetros do tipo covariante (out) e Bot é o tipo inferior (subtipo de qualquer outro tipo, oposto ao Object). Eu não acho que o c # tenha um tipo inferior.Em C #, você não pode ter esse
Empty
tipo, porque, devido à reificação, os tipos de base são diferentes para diferentes tipos de membros. Você só pode terEmpty<T>
; não é tão útil.Em Java, você pode ter
Empty : ConsList
devido ao apagamento do tipo, mas não tenho certeza se o verificador de tipos não gritaria em algum lugar.No entanto, como os dois idiomas têm
null
, você pode pensar em todos os tipos de referência como sendo "Whatever | Null". Então, basta usar onull
como "Vazio" para evitar precisar especificar o que deriva.fonte
null
é que é muito geral: representa a ausência de qualquer coisa , ou seja, o vazio em geral, mas quero representar a ausência de elementos da lista, ou seja, uma lista vazia em particular. Uma lista vazia e uma árvore vazia devem ter tipos distintos. Além disso, a lista vazia precisa ser um valor real porque ainda possui comportamento próprio, portanto, precisa ter seus próprios métodos. Para construir a lista[1, 2, 3]
, quero dizerEmpty.prepend(3).prepend(2).prepend(1)
(ou em um idioma com operadores associativos à direita1 :: 2 :: 3 :: Empty
), mas não posso dizernull.prepend …
.Empty
e umEmpty<>
operador de conversão implícito e abusar para permitir uma simulação bastante prática, se desejar. Essencialmente, você usaEmpty
no código, mas todas as assinaturas de tipo, etc., usam apenas as variantes genéricas.Em Java você não pode. Mas você pode declarar a classe base como pacote privado, o que significa que todas as subclasses diretas devem pertencer ao mesmo pacote que a classe base. Se você declarar as subclasses como finais, elas não poderão mais ser subclasses.
Não sei se isso resolveria o seu problema real ...
fonte
intanceof
verificações dinâmicas "seguros por pseudo-tipo" (ou seja: eu sei que é seguro, mesmo que o compilador não o faça), simplesmente garantindo que eu sempre verifique esses dois casos. Se, no entanto, alguém adicionar uma nova subclasse, posso obter erros de tempo de execução que não esperava.O tipo de dados
ConsList<A>
pode ser representado como uma interface. A interface expõe umdeconstruct
método único que permite "desconstruir" um valor desse tipo - ou seja, manipular cada um dos possíveis construtores. As chamadas para umdeconstruct
método são análogas a umcase of
formulário em Haskell ou ML.O
deconstruct
método utiliza uma função de "retorno de chamada" para cada construtor no ADT. No nosso caso, é necessária uma função para o caso de lista vazio e outra função para o caso "cons cell".Cada função de retorno de chamada aceita como argumentos os valores que são aceitos pelo construtor. Portanto, o caso "lista vazia" não aceita argumentos, mas o caso "cons cell" usa dois argumentos: o cabeçalho e o final da lista.
Podemos codificar esses "múltiplos argumentos" usando
Tuple
classes ou usando currying. Neste exemplo, eu escolhi usar umaPair
classe simples .A interface é implementada uma vez para cada construtor. Primeiro, temos a implementação da "lista vazia". A
deconstruct
implementação simplesmente chama aemptyCase
função de retorno de chamada.Em seguida, implementamos o caso "cons cell" da mesma forma. Desta vez, a classe tem propriedades: a cabeça e a cauda da lista não vazia. Na
deconstruct
implementação, essas propriedades são passadas para aconsCase
função de retorno de chamada.Aqui está um exemplo do uso dessa codificação de ADTs: podemos escrever uma
reduce
função que é a lista de dobras comuns.Isso é análogo a esta implementação em Haskell:
fonte
Não há uma boa maneira de fazer isso, mas se você estiver disposto a conviver com uma invasão hedionda, poderá adicionar alguma verificação explícita de tipo ao construtor da classe base abstrata. Em Java, isso seria algo como
No C #, é mais complicado por causa dos genéricos reificados - a abordagem mais simples pode ser converter o tipo em uma string e alterá-la.
Observe que em Java, mesmo esse mecanismo pode teoricamente ser ignorado por alguém que realmente deseja através do modelo de serialização ou
sun.misc.Unsafe
.fonte
Type type = this.GetType(); if (type != typeof(Empty<T>) && type != typeof(ConsCell<T>)) throw new Exception();