Existe uma maneira bastante "padrão" de codificar tipos de soma em uma linguagem orientada a objetos.
Aqui estão dois exemplos:
type Either<'a, 'b> = Left of 'a | Right of 'b
Em C #, poderíamos renderizar isso 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 # novamente:
type List<'a> = Nil | Cons of 'a * List<'a>
C # novamente:
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);
}
}
A codificação é completamente mecânica. Essa codificação produz um resultado com as mesmas vantagens e desvantagens dos tipos de dados algébricos. Você também pode reconhecer isso como uma variação do padrão de visitantes. Poderíamos coletar os parâmetros Match
juntos em uma interface que poderíamos chamar de Visitante.
Do lado das vantagens, isso fornece uma codificação baseada em princípios de tipos de soma. (É a codificação Scott .) Fornece uma "correspondência de padrões" exaustiva, embora apenas uma "camada" de correspondência de cada vez. Match
é, de certa forma, uma interface "completa" para esses tipos e quaisquer operações adicionais que desejamos podem ser definidas em termos disso. Apresenta uma perspectiva diferente sobre muitos padrões de OO, como o Padrão de Objeto Nulo e o Padrão de Estado, como indiquei na resposta de Ryathal, bem como o Padrão de Visitante e o Padrão de Composição. O tipo Option
/ Maybe
é como um padrão de objeto nulo genérico. O padrão composto é semelhante à codificação type Tree<'a> = Leaf of 'a | Children of List<Tree<'a>>
. O padrão de estado é basicamente uma codificação de uma enumeração.
Do lado das desvantagens, como escrevi, o Match
método impõe algumas restrições sobre quais subclasses podem ser adicionadas de maneira significativa, especialmente se queremos manter a propriedade de substituibilidade de Liskov. Por exemplo, aplicar essa codificação a um tipo de enumeração não permitiria estender significativamente a enumeração. Se você quisesse estender a enumeração, teria que alterar todos os chamadores e implementadores em todos os lugares, como se estivesse usando enum
e switch
. Dito isto, essa codificação é um pouco mais flexível que a original. Por exemplo, podemos adicionar um Append
implementador List
que apenas contém duas listas, fornecendo um anexo de tempo constante. Isso se comportaria como as listas anexadas, mas seria representado de uma maneira diferente.
Obviamente, muitos desses problemas têm a ver com o fato de que Match
está um pouco (conceitual mas intencionalmente) vinculado às subclasses. Se usarmos métodos não tão específicos, obteremos projetos de OO mais tradicionais e recuperaremos a extensibilidade, mas perderemos a "integridade" da interface e, portanto, perderemos a capacidade de definir qualquer operação nesse tipo em termos de interface. Como mencionado em outro lugar, essa é uma manifestação do Problema da Expressão .
Pode-se argumentar que projetos como o acima podem ser usados sistematicamente para eliminar completamente a necessidade de ramificação, sempre atingindo um ideal de OO. Smalltalk, por exemplo, usa esse padrão frequentemente incluindo os próprios booleanos. Mas, como sugere a discussão anterior, essa "eliminação da ramificação" é bastante ilusória. Acabamos de implementar a ramificação de uma maneira diferente e ela ainda possui muitas das mesmas propriedades.