Hay una forma bastante "estándar" de codificar tipos de suma en un lenguaje orientado a objetos.
Aquí hay dos ejemplos:
type Either<'a, 'b> = Left of 'a | Right of 'b
En C #, podríamos representar esto como:
interface Either<A, B> {
C Match<C>(Func<A, C> left, Func<B, C> right);
}
class Left<A, B> : Either<A, B> {
private readonly A a;
public Left(A a) { this.a = a; }
public C Match<C>(Func<A, C> left, Func<B, C> right) {
return left(a);
}
}
class Right<A, B> : Either<A, B> {
private readonly B b;
public Right(B b) { this.b = b; }
public C Match<C>(Func<A, C> left, Func<B, C> right) {
return right(b);
}
}
F # nuevamente:
type List<'a> = Nil | Cons of 'a * List<'a>
C # nuevamente:
interface List<A> {
B Match<B>(B nil, Func<A, List<A>, B> cons);
}
class Nil<A> : List<A> {
public Nil() {}
public B Match<B>(B nil, Func<A, List<A>, B> cons) {
return nil;
}
}
class Cons<A> : List<A> {
private readonly A head;
private readonly List<A> tail;
public Cons(A head, List<A> tail) {
this.head = head;
this.tail = tail;
}
public B Match<B>(B nil, Func<A, List<A>, B> cons) {
return cons(head, tail);
}
}
La codificación es completamente mecánica. Esta codificación produce un resultado que tiene la mayoría de las mismas ventajas y desventajas de los tipos de datos algebraicos. También puede reconocer esto como una variación del Patrón de visitante. Podríamos recopilar los parámetros Matchjuntos en una interfaz que podríamos llamar un visitante.
En el lado de las ventajas, esto le proporciona una codificación basada en principios de tipos de suma. (Es la codificación Scott ). Le brinda una exhaustiva "coincidencia de patrones" aunque solo una "capa" de coincidencia a la vez. Matches de alguna manera una interfaz "completa" para estos tipos y cualquier operación adicional que deseemos puede definirse en términos de la misma. Presenta una perspectiva diferente en muchos patrones OO, como el Patrón de objeto nulo y el Patrón de estado, como indiqué en la respuesta de Ryathal, así como el Patrón de visitante y el Patrón compuesto. El tipo Option/ Maybees como un patrón de objeto nulo genérico. El patrón compuesto es similar a la codificación type Tree<'a> = Leaf of 'a | Children of List<Tree<'a>>. El patrón de estado es básicamente una codificación de una enumeración.
En el lado de las desventajas, como lo escribí, el Matchmétodo impone algunas restricciones sobre qué subclases se pueden agregar significativamente, especialmente si queremos mantener la Propiedad de sustituibilidad de Liskov. Por ejemplo, aplicar esta codificación a un tipo de enumeración no le permitiría extender significativamente la enumeración. Si quisiera extender la enumeración, tendría que cambiar todas las personas que llaman e implementadores en todas partes como si estuviera usando enumy switch. Dicho esto, esta codificación es algo más flexible que la original. Por ejemplo, podemos agregar un Appendimplementador Listque solo contiene dos listas, lo que nos da un apéndice de tiempo constante. Esto se comportaría como las listas adjuntas, pero se representaría de manera diferente.
Por supuesto, muchos de estos problemas tienen que ver con el hecho de que Matchestá un tanto (conceptual pero intencionalmente) vinculado a las subclases. Si utilizamos métodos que no son tan específicos, obtenemos diseños OO más tradicionales y recuperamos la extensibilidad, pero perdemos la "integridad" de la interfaz y, por lo tanto, perdemos la capacidad de definir cualquier operación en este tipo en términos de interfaz. Como se mencionó en otra parte, esta es una manifestación del problema de expresión .
Podría decirse que los diseños como los anteriores se pueden usar sistemáticamente para eliminar por completo la necesidad de ramificar para lograr un ideal OO. Smalltalk, por ejemplo, usa este patrón a menudo incluso para los propios booleanos. Pero como sugiere la discusión anterior, esta "eliminación de ramificación" es bastante ilusoria. Acabamos de implementar la ramificación de una manera diferente, y todavía tiene muchas de las mismas propiedades.