Criando mapas "apenas" com funções em java
Bem, já pensou se fosse legal fazer um mapeamento em Java usando
apenas funções? É útil? Bem, sinceramente? Não vejo muita utilidade,
o HashMap
continua sendo uma linda implementação para mapeamentos.
Mas vale o exercício, não vale?
Definindo o problema
Pegue uma função, , onde . Isso significa que é uma função (potencialmente) parcial de .
Então, como fazer para que se comporte de modo “completo” em ? Uma maneira é pegar o elemento nulo . Então, podemos definir
Como definimos ela? Bem…
Com isso, temos uma função de mapeamento parcial de e transformamos, sem perder semântica e também de modo unívoco, em uma função total de .
Agora, o que eu preciso é criar novos mapeamentos baseados em funções parciais do tipo . Eu vou definir primeiro uma função que remove um mapeamento prévio para um elemento .
Bem, se eu quero remover o mapeamento, eu posso também fazer um overwrite do mapeamento e detectar que, ao chamar , deveria retornar . Então, assim conseguimos definir a função .
Ok, eu tenho agora a função . Agora, eu posso criar uma função que adiciona no mapeamento, algo como :
A partir de qualquer função parcial de eu posso derivar novos mapeamentos. E… qual seria o mapeamento mais simples? O caso base do qual podemos derivar todos os casos possíveis: o caso em que . Esse é o caso da funcão . A função total é derivada assim:
Escrevendo as funções em si
Começando pela função que cria o mapeamento vazio:
Agora, a função que remove mapeamentos:
Finalmente, a função que adiciona mapeamentos:
Variante: objeto matemático com mapeamento e conjunto domínio
Bem, e se o objeto matemático, além da função, tivesse um campo
chamado dom
com os elementos de ? Assim, poderíamos
saber não apenas que , mas
literamente conhecer . Será que seria possível manter essa
propriedade junto das alterações no mapeamento?
Bem, começar pela remoção. Eu tenho um conjunto que potencialmente tem o elemento e, após a remoção do mapeamento, não terá mais esse elemento no domínio:
E adicionar um mapeamento? Bem, nesse caso específico não precisamos saber o valor para o qual será mapeado, apenas a chave. E o bom é que, como a chave pode sobrescrever algo mapeado anteriormente, não existe problema nisso.
Fazendo em Java
Bem, já que queremos usar funções para tudo, que tal começarmos escrevendo uma interface? Parece o tipo adequado, não é?
interface Mapeamento<D,C> {
C mapear(D chave);
Set<D> dominio();
}
Antes de começar, já que estamos lidando com funções, que tal ter uma espécie de “construtor” utilitário para nos auxiliar? Um que receba duas funções, uma de e outro que nos forneça ?
static <D, C> Mapeamento<D,C> criarMapeamento(Function<D, C> f, Supplier<Set<D>> dominio) {
return new Mapeamento<>() {
@Override
public C mapear(D chave) {
return f.apply(chave);
}
@Override
public Set<D> dominio() {
return dominio.get();
}
};
}
Bem, aqui podemos assumir a ausência de valor como null
, no mundo
do Java. Vamos criar a função que faz o emptyMapping
?
static <D, C> Mapeamento<D,C> mapeamentoVazio() {
return criarMapeamento(unused -> null, Set::of);
}
Para remover um mapeamento? Bem, vamos lá. Não vou tentar fazer a melhor solução, nem tampouco que seja eficiente, apenas que reflita a necessidade matemática por trás, que foi mapeada anteriormente:
static <D, C> Mapeamento<D,C> removerMapeamento(Mapeamento<D, C> e, D chaveRemocao) {
return criarMapeamento(
d -> chaveRemocao.equals(d)? null: e.mapear(d),
() -> {
HashSet<D> novoDominio = new HashSet<>(e.dominio());
novoDominio.remove(chaveRemocao);
return Collections.unmodifiableSet(novoDominio);
}
);
}
E para adicionar um novo mapeamento?
static <D, C> Mapeamento<D,C> adicionarMapeamento(Mapeamento<D, C> e, D chaveAdicionada, C novoValor) {
return criarMapeamento(
d -> chaveAdicionada.equals(d)? novoValor: e.mapear(d),
() -> {
HashSet<D> novoDominio = new HashSet<>(e.dominio());
novoDominio.add(chaveAdicionada);
return Collections.unmodifiableSet(novoDominio);
}
);
}
Alternativas para evitar mexer no estado de novoDominio
Na remoção, poder-se-ia usar stream
:
e.dominio().stream()
.filter(d -> !chaveRemocao.equals(d))
.collect(Collectors.toSet());
Para adição, poderíamos juntar duas streams:
Stream.concat(e.dominio().stream(), Arrays.stream(chaveAdicionada))
.collect(Collectors.toSet());
Por que não passar Set<D>
como valor?
Bem, estamos em Java. Alguém pode querer criar algo stateful
que implemente Mapeamento
. Se eu recebesse um conjunto “duro”,
possivelmente não seria da referência mutável. Então, para evitar
isso, para evitar ter a referência guardada, é melhor sempre
obter a nova referência.