O tipo de dados ConsList<A>
pode ser representado como uma interface. A interface expõe um único método deconstruct
que permite "desconstruir" um valor desse tipo - isto é, manipular cada um dos possíveis construtores. As chamadas para um método deconstruct
são análogas a uma forma case of
em Haskell ou ML.
interface ConsList<A> {
<R> R deconstruct(
Function<Unit, R> emptyCase,
Function<Pair<A,ConsList<A>>, R> consCase
);
}
O método deconstruct
recebe 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 vazia 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 recebe argumentos, mas o caso "cons cell" recebe dois argumentos: o cabeçalho e o final da lista.
Podemos codificar esses "vários argumentos" usando Tuple
classes ou usando currying. Neste exemplo, escolhi usar uma simples classe Pair
.
A interface é implementada uma vez para cada construtor. Primeiro, temos a implementação da "lista vazia". A implementação deconstruct
simplesmente chama a função de retorno de chamada emptyCase
.
class ConsListEmpty<A> implements ConsList<A> {
public ConsListEmpty() {}
public <R> R deconstruct(
Function<Unit, R> emptyCase,
Function<Pair<A,ConsList<A>>, R> consCase
) {
return emptyCase.apply(new Unit());
}
}
Em seguida, implementamos o caso "cons cell" de maneira semelhante. Desta vez, a classe tem propriedades: a cabeça e cauda da lista não vazia. Na implementação deconstruct
, essas propriedades são passadas para a função de retorno de chamada consCase
.
class ConsListConsCell<A> implements ConsList<A> {
private A head;
private ConsList<A> tail;
public ConsListCons(A head, ConsList<A> tail) {
this.head = head;
this.tail = tail;
}
public <R> R deconstruct(
Function<Unit, R> emptyCase,
Function<Pair<A,ConsList<A>>, R> consCase
) {
return consCase.apply(new Pair<A,ConsList<A>>(this.head, this.tail));
}
}
Aqui está um exemplo de como usar essa codificação de ADTs: podemos escrever uma função reduce
que é a lista normal de dobras.
<T> T reduce(Function<Pair<T,A>,T> reducer, T initial, ConsList<T> l) {
return l.deconstruct(
((unit) -> initial),
((t) -> reduce(reducer, reducer.apply(initial, t.v1), t.v2))
);
}
Isso é análogo a essa implementação em Haskell:
reduce reducer initial l = case l of
Empty -> initial
Cons t_v1 t_v2 -> reduce reducer (reducer initial t_v1) t_v2