Hay algunos problemas que los tipos de datos algebraicos pueden resolver fácilmente, por ejemplo, un tipo de lista puede expresarse de manera muy 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 ejemplo particular está en Haskell, pero sería similar en otros idiomas con soporte nativo para tipos de datos algebraicos.
Resulta que hay un mapeo obvio para el subtipo de estilo OO: el tipo de datos se convierte en una clase base abstracta y cada constructor de datos se convierte en una subclase concreta. Aquí hay un ejemplo en 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+)
Lo único que se necesita más allá de las subclases ingenuas es una forma de sellar clases, es decir, una forma de hacer que sea imposible agregar subclases a una jerarquía.
¿Cómo abordarías este problema en un lenguaje como C # o Java? Los dos escollos que encontré al intentar usar tipos de datos algebraicos en C # fueron:
- No pude averiguar cómo se llama el tipo de fondo en C # (es decir, no pude averiguar en qué poner
class Empty : ConsList< ??? >
) - No pude encontrar una manera de sellar
ConsList
para que no se puedan agregar subclases a la jerarquía
¿Cuál sería la forma más idiomática de implementar tipos de datos algebraicos en C # y / o Java? O, si no es posible, ¿cuál sería el reemplazo idiomático?
Respuestas:
Hay una manera fácil, pero pesada, de sellar las clases en Java. Pones un constructor privado en la clase base y luego haces subclases de clases internas.
Agregue un patrón de visitante para su envío.
Mi proyecto jADT: Java Algebraic DataTypes genera toda esa plantilla para ti https://github.com/JamesIry/jADT
fuente
Either
. Ver mi preguntaPuede lograr esto utilizando el patrón de visitante , que complementará la coincidencia de patrones. Por ejemplo
se puede escribir en Java como
El sellado se logra por la
Visitor
clase. Cada uno de sus métodos declara cómo deconstruir una de las subclases. Podría agregar más subclases, pero tendría que implementaraccept
y llamando a uno de losvisit...
métodos, por lo que tendría que comportarse comoCons
o comoNil
.fuente
Si abusa de los parámetros con nombre C # (introducidos en C # 4.0), puede crear tipos de datos algebraicos que sean fáciles de combinar en:
Aquí está la implementación de la
Either
clase:fuente
class Right<R> : Either<Bot,R>
donde Either se cambia a una interfaz con parámetros de tipo covariante (out), y Bot es el tipo inferior (subtipo de cualquier otro tipo, opuesto a Object). No creo que C # tenga un tipo inferior.En C #, no puede tener ese
Empty
tipo porque, debido a la reificación, los tipos base son diferentes para los diferentes tipos de miembros. Solo puedes tenerEmpty<T>
; No es tan útil.En Java, puede
Empty : ConsList
deberse a la eliminación de tipo, pero no estoy seguro de si el verificador de tipo no gritaría en alguna parte.Sin embargo, dado que ambos idiomas tienen
null
, puede pensar en todos sus tipos de referencia como "Cualquiera | Nulo". Por lo tanto, utilizaría elnull
como "Vacío" para evitar tener que especificar de qué deriva.fuente
null
es que es demasiado general: representa la ausencia de algo , es decir, el vacío en general, pero quiero representar la ausencia de elementos de la lista, es decir, una lista vacía en particular. Una lista vacía y un árbol vacío deben tener distintos tipos. Además, la lista vacía debe ser un valor real porque todavía tiene un comportamiento propio, por lo que debe tener sus propios métodos. Para construir la lista[1, 2, 3]
, quiero decirEmpty.prepend(3).prepend(2).prepend(1)
(o en un lenguaje con operadores asociativos correctos1 :: 2 :: 3 :: Empty
), pero no puedo decirnull.prepend …
.Empty
y anEmpty<>
y abusos para permitir una simulación bastante práctica, si lo desea. Básicamente, se usaEmpty
en código, pero todas las firmas de tipo, etc., solo usan variantes genéricas.En Java no puedes. Pero puede declarar la clase base como paquete privado, lo que significa que todas las subclases directas deben pertenecer al mismo paquete que la clase base. Si luego declaras las subclases como finales, no se pueden subclasificar más.
Sin embargo, no sé si esto resolvería tu problema real ...
fuente
intanceof
verificaciones dinámicas "pseudo-type-safe" (es decir: sé que es seguro, incluso si el compilador no lo hace), simplemente asegurándome de que siempre revisa esos dos casos. Sin embargo, si alguien agrega una nueva subclase, entonces puedo obtener errores de tiempo de ejecución que no esperaba.El tipo de datos
ConsList<A>
se puede representar como una interfaz. La interfaz expone un únicodeconstruct
método que le permite "deconstruir" un valor de ese tipo, es decir, manejar cada uno de los posibles constructores. Las llamadas a undeconstruct
método son análogas a uncase of
formulario en Haskell o ML.El
deconstruct
método toma una función de "devolución de llamada" para cada constructor en el ADT. En nuestro caso, toma una función para el caso de la lista vacía, y otra función para el caso de "celda de contras".Cada función de devolución de llamada acepta como argumentos los valores que acepta el constructor. Entonces, el caso de la "lista vacía" no toma argumentos, pero el caso de la "celda de contras" toma dos argumentos: el encabezado y el final de la lista.
Podemos codificar estos "argumentos múltiples" usando
Tuple
clases, o usando curry. En este ejemplo, elegí usar unaPair
clase simple .La interfaz se implementa una vez para cada constructor. Primero, tenemos la implementación de la "lista vacía". La
deconstruct
implementación simplemente llama a laemptyCase
función de devolución de llamada.Luego implementamos el caso "cons cell" de manera similar. Esta vez la clase tiene propiedades: el encabezado y la cola de la lista no vacía. En la
deconstruct
implementación, esas propiedades se pasan a laconsCase
función de devolución de llamada.Aquí hay un ejemplo del uso de esta codificación de ADT: podemos escribir una
reduce
función que es la lista de plegado habitual.Esto es análogo a esta implementación en Haskell:
fuente
No hay una buena manera de hacer esto, pero si estás dispuesto a vivir con un truco horrible, entonces puedes agregar alguna comprobación de tipo explícito al constructor de la clase base abstracta. En Java, esto sería algo así como
En C # es más complicado debido a los genéricos reificados: el enfoque más simple podría ser convertir el tipo en una cadena y destrozar eso.
Tenga en cuenta que, en Java, incluso este mecanismo puede ser evitado teóricamente por alguien que realmente quiera hacerlo a través del modelo de serialización o
sun.misc.Unsafe
.fuente
Type type = this.GetType(); if (type != typeof(Empty<T>) && type != typeof(ConsCell<T>)) throw new Exception();