DEV Community

Angelo Belchior
Angelo Belchior

Posted on

Discriminated Unions: Essa feature faz falta ao CSharp

imagem com uma capivara triste com uma camisa escrita csharp olhando para o céu ao lado de um balanço infantil

A versão 1.0 do csharp foi lançada em janeiro de 2002. As principais features eram Classes,
Structs, Interfaces, Events, Properties, Delegates, Operators and expressions, Statements e Attributes.
Logo de cara era inevitável a comparação com o Java. E fazia total sentido, afinal Anders Hejlsberg saíra da Borland em 1996 para trabalhar no projeto J++ e Microsoft Java Virtual Machine, o que era basicamente a implementação da Microsoft para o Java.

Deu ruim.

A finada Sun não gostou muito a ideia e rolaram diversos processos. A Microsoft ainda investiu tempo e esforço no famigerado J#, mas a coisa só engrenou mesmo com o nosso querido e amado CSharp.


Antes de continuar, #VemCodar com a gente!!

Tá afim de criar APIs robustas com .NET?

Feito de dev para dev, direto das trincheiras, por quem coloca software em produção há mais de 20 anos, o curso "APIs robustas com ASP.NET Core 8, Entity Framework, Docker e Redis" traz de maneira direta, sem enrolação, tudo que você precisa saber para construir suas APIs de tal forma que elas sejam seguras, performáticas, escaláveis e, acima de tudo, dentro de um ambiente de desenvolvimento totalmente configurado utilizando Docker e Docker Compose.

Não fique de fora! Dê um Up na sua carreira!

O treinamento acontecerá nos dias 27/02, 28/02 e 29/02, online, das 19.30hs às 22.30hs

Acesse: https://vemcodar.com.br/


Porém, mesmo com toda essa semelhança, o CSharp trazia logo de cara duas coisas fantásticas que não existiam no Java (e acho que até hoje não existem): Properties e Delegates.
Aliás, a própria Sun rejeitou a ideia proposta pela Microsoft de se ter delegates no Java.

Eu tentei achar um artigo da época onde a Sun falava sobre isso, mas infelizmente não encontrei.

Dando uma googlada achei um blog post de 2009 do nosso querido Ben Hutchison falando sobre o tema: Sun’s rejection of Delegates for Java | My Digital Neuron (wordpress.com) .

Eu destaco aqui um trecho que o próprio autor destacou em seu post:

A versão mais recente do ambiente de desenvolvimento Microsoft Visual J++ suporta uma construção de linguagem chamada delegates ou método vinculado. (…) É improvável que o Java inclua esta construção. (…) referências de métodos vinculados são desnecessárias e prejudicial à linguagem, a simplicidade e o caráter amplamente orientado a objetos das APIs.

Eu só não entendi quando eles citam "prejudicam a simplicidade do Java". Já compararam a forma como o Java resolve a ideia do delegate com o que temos no CSharp? Enfim...

Eu fiz essa introdução meio provocativa apenas para tentar trazer um conceito histórico de que o time de design e desenvolvimento da linguagem CSharp sempre procurou trazer inovações.

LINQ, async/await, dinamycs, partial class/methods, e mais atualmente todo um suporte a conceitos de linguagem funcional como pattern matching, records e etc, são exemplos disso.

Se você acessar esse link The history of C# - C# Guide - C# | Microsoft Learn vai ver todos os principais recursos introduzidos em cada versão lançada da linguagem.

Pois bem, mesmo com todos esses recursos, mesmo evoluindo ano após ano, tem algo que me deixa um pouco frustrado com a linguagem: A ausência de Discriminated Unions. (Também conhecido como Tagged Union ou Choice Type).

Esse é o tipo de feature que depois que você entende, sua mente explode e você se pergunta: Por que eu não usei isso antes?

Para o caso do CSharp a resposta é simples: Porque a linguagem não suporta (Ok, tem "formas" de se ter esse tipo de implementação e eu mostro algumas delas mais abaixo).

Acredito que bateu uma curiosidade para saber por que causa, motivo, razão ou circunstância essa feature faz falta ao CSharp, certo?

Como de costume, pega aquele café e vem comigo.

Você deve saber que o dotnet suporta algumas linguagens de programação. Além do CSharp, temos o VB.Net e o F#.
Curiosamente, o F# suporta Discriminated Unions e é baseada nessa documentação que vamos refletir um pouco a respeito dessa feature bacanuda que faz falta.

Discriminated Unions fornecem suporte para valores que podem ser um entre vários casos, possivelmente cada um com valores e tipos diferentes. Os Discriminated Unions são úteis para dados heterogéneos; dados que podem ter casos especiais, incluindo casos válidos e de erro; dados que variam em tipo de uma instância para outra; e como alternativa para hierarquias de objetos pequenos. Além disso, Discriminated Unions recursivas são usadas para representar estruturas de dados em árvore.

Para tentar explicar em código, o que a frase acima diz, vamos imaginar o seguinte cenário e em seguida implementá-lo de diversas formas:

Eu tenho uma classe responsável por administrar solicitações de aumento de limite de crédito. Essa classe tem um método que recebe o Id do Cliente e faz uma busca no banco de dados:

  • Caso o cliente não exista, o método deve retornar o status de Cliente não encontrado.
  • Caso o cliente esteja inativado, o método retorna que não é possível efetuar aumento de Limite de Crédito.
  • Caso o cliente já tenha feito uma solicitação, o método retorna que o processo está em análise.
  • Caso o tipo do cliente seja Comum e o valor do limite seja maior que R$1000, o sistema bloqueia.
  • Caso o tipo do cliente seja Especial e o valor do limite seja maior que R$1500, o sistema bloqueia.
  • Caso o tipo do cliente seja Premium e o valor do limite seja maior que R$2000, o sistema bloqueia.
  • Por fim, se todas as condições acima forem favoráveis, a solicitação é enviada para análise.

A ideia aqui é ser didático e o exemplo foi feito sem nenhuma responsabilidade :)

A primeira pergunta que eu faço aqui é, como podemos mapear todos esses tipos de retorno?
Talvez criar uma classe contemplando todos as possibilidades, seja uma alternativa, certo?

Vamos a um exemplo:

public ResultadoDaSolitacao SolicitarAumento(Guid idDoCliente, NovoLimiteSolicitado novoLimiteSolicitado)
{
    var cliente = clienteRepository.ObterPorId(idDoCliente);
    if(cliente is null)
        return new ResultadoDaSolitacao 
        { 
            Status = TipoResultado.ClienteNaoEncontrado, 
        };

    if (cliente.EstaInativado)
        return new ResultadoDaSolitacao 
        { 
            Status = TipoResultado.ClienteInativado, 
            ValorDevido = cliente.ValorDevido 
        };

    if(cliente.EmAnalise)
        return new ResultadoDaSolitacao
        {
            Status = TipoResultado.ClienteEmAnalise, 
            DataDaSolicitacao = cliente.DataDaSolicitacao
        };

    if(cliente.Tipo == TipoCliente.Comum && novoLimiteSolicitado.Valor > 1000)
        return new ResultadoDaSolitacao
        {
            Status = TipoResultado.LimiteSolicitadoExcedeLimiteMaximo,
            LimiteExcedido = new LimiteExcedido(cliente.Tipo, novoLimiteSolicitado.Valor, 1000)
        };

    if(cliente.Tipo == TipoCliente.Especial && novoLimiteSolicitado.Valor > 1500)
        return new ResultadoDaSolitacao
        {
            Status = TipoResultado.LimiteSolicitadoExcedeLimiteMaximo,
            LimiteExcedido = new LimiteExcedido(cliente.Tipo, novoLimiteSolicitado.Valor, 1500)
        };

    if(cliente.Tipo == TipoCliente.Premium && novoLimiteSolicitado.Valor > 2000)
        return new ResultadoDaSolitacao
        {
            Status = TipoResultado.LimiteSolicitadoExcedeLimiteMaximo,
            LimiteExcedido = new LimiteExcedido(cliente.Tipo, novoLimiteSolicitado.Valor, 2000)
        };

    EnviarParaAnalise(cliente, novoLimiteSolicitado);
    return new ResultadoDaSolitacao { Status = TipoResultado.SolicitacaoEnviada };
}

public class ResultadoDaSolitacao  
{  
  public Status Resultado { get; init; }  
  public DateTime? DataDaSolicitacao { get; init; }  
  public LimiteExcedido? LimiteExcedido { get; init; } 
  public decimal? ValorDevido { get; init; }   
}  

public record LimiteExcedido(TipoCliente Tipo, decimal ValorSolicitado, decimal LimiteMaximo);
Enter fullscreen mode Exit fullscreen mode

Se analisarmos friamente, o método SolicitarAumento pode retornar cinco situações distintas:

  • Cliente não encontrado
  • Cliente Inativado
  • Cliente em análise
  • Solicitação de valor excede limite permitido
  • Solicitação enviada para análise

E para que a gente pudesse mapear todos esses valores, criamos a classe ResultadoDaSolitacao, que tem um status indicando em qual situação se encontra a execução da solicitação de aumento de limite de crédito.

Essa classe se faz necessária pois cada status pode ter um tipo de informação pertinente. Por exemplo, se a pessoa está inativada, retornamos o valor devido ao banco.

Se o limite solicitado excede o limite disponível para o tipo de pessoa, retornamos uma estrutura indicando o valor solicitado e o valor máximo permitido.

Existem outras formas de se resolver isso, mas geralmente a gente cai nesse cenário, criando uma classe, que mais parece a curva de um rio, contendo várias informações que muitas vezes não tem relações umas com as outras.

Se a gente for analisar, a informação de limite excedido não tem relação nenhuma com o limite devido. São universos distintos. E esse talvez seja o menor dos problemas...

Quem consome esse método precisa entender toda essa estrutura de retorno para poder saber, de fato, qual será a resposta a ser dada para quem efetuou a requisição. E é aqui que mora o perigo maléfico.

Analise o código abaixo:

[HttpPost("/clientes/{idCliente}/limite-de-credito/solicitar-aumento")]
public IActionResult SolicitarAumento(Guid idCliente, NovoLimiteSolicitado novoLimiteSolicitado)
{
    var processador = new ProcessadorDeLimiteDeCredito(_clienteRepository);
    var resultado = processador.SolicitarAumento(idCliente, novoLimiteSolicitado);

    if(resultado.Status == TipoResultado.ClienteNaoEncontrado)
        return Results.NotFound(new { idCliente = idCliente });

    if (resultado.Status == TipoResultado.ClienteInativado)
        return Results.BadRequest(new { status = TipoResultado.SolicitacaoEnviada, valorDevido = resultado.ValorDevido });    

    if (resultado.Status == TipoResultado.ClienteEmAnalise)
        return Results.Accepted(new {status = TipoResultado.SolicitacaoEnviada });

    if (resultado.Status == TipoResultado.LimiteSolicitadoExcedeLimiteMaximo)
        return Results.BadRequest(new {
            Status = TipoResultado.LimiteSolicitadoExcedeLimiteMaximo,
            LimiteExcedido = resultado.LimiteExcedido
        });

    return Results.Created($"/clientes/{idCliente}/limite-de-credito/solicitacoes/{resultado.IdSolicitacao}");
}
Enter fullscreen mode Exit fullscreen mode

Esse é um cenário muito comum. Acredito que você já tenha visto algo bem parecido ou até mesmo implementado algo nessa linha.

Bem, vamos retomar a definição dos Discriminated Unions.

Discriminated Unions fornecem suporte para valores que podem ser um entre vários casos, possivelmente cada um com valores e tipos diferentes.

Avaliando essa frase, podemos imaginar como essa feature conseguiria nos ajudar a resolver o cenário mostrado de forma mais limpa e objetiva?

Veja, a definição carrega algo muito importante: "Discriminated Unions fornecem suporte para valores que podem ser um entre vários casos.".

Essa frase descreve perfeitamente a nossa real necessidade para implementar as funcionalidades do exemplo apresentado: "... suporte para valores que podem ser um entre vários casos."

É exatamente disso que precisamos e exatamente isso que não existe no CSharp.

Nós precisamos representar o resultado de processamento de cinco formas diferentes e cada forma contendo suas características. Cada uma delas poderia ser uma classe própria, certo?

Algo como:

public record ClienteNaoEncontrado(Guid IdCliente);  
public record ClienteInativado(Guid IdCliente, decimal ValorDevido);  
public record ClienteEmAnalise(Guid IdCliente, DateTime DataDaSolicitacao);  
public record LimiteSolicitadoExcedeLimiteMaximo(TipoCliente Tipo, decimal ValorSolicitado, decimal LimiteMaximo);
public record LimiteSolicitado(Guid IdSolicitacao);
Enter fullscreen mode Exit fullscreen mode

Mas como eu poderia retornar um dos cinco tipos acima? Herança, Polimorfismo?

Já sei, tuplas!!

Gif de um ator que eu não lembro o nome falando não

Eu já vi pessoas sugerirem tuplas para resolver isso, mas a implementação em si fica absolutamente horrível:

public (ClienteNaoEncontrado?, ClienteInativado?, ClienteEmAnalise?, LimiteSolicitadoExcedeLimiteMaximo?, LimiteSolicitado?) 
    SolicitarAumentoComTuplas(Guid idDoCliente, NovoLimiteSolicitado novoLimiteSolicitado)
{
    var cliente = clienteRepository.ObterPorId(idDoCliente);
    if(cliente is null)
        return (new ClienteNaoEncontrado(idDoCliente), null, null, null, null);

    if (cliente.EstaInativado)
        return (null, new ClienteInativado(idDoCliente, cliente.ValorDevido), null, null, null);

    if(cliente.EmAnalise)
        return (null, null, new ClienteEmAnalise(idDoCliente, cliente.DataDaSolicitacao.GetValueOrDefault()), null, null);

    if(cliente.Tipo == TipoCliente.Comum && novoLimiteSolicitado.Valor > 1000)
        return (null, null, null, new LimiteSolicitadoExcedeLimiteMaximo(cliente.Tipo, novoLimiteSolicitado.Valor, 1000), null);

    if(cliente.Tipo == TipoCliente.Especial && novoLimiteSolicitado.Valor > 1500)
        return (null, null, null, new LimiteSolicitadoExcedeLimiteMaximo(cliente.Tipo, novoLimiteSolicitado.Valor, 1500), null);

    if(cliente.Tipo == TipoCliente.Premium && novoLimiteSolicitado.Valor > 2000)
        return (null, null, null, new LimiteSolicitadoExcedeLimiteMaximo(cliente.Tipo, novoLimiteSolicitado.Valor, 2000), null);

    var idDaSolicitacao = EnviarParaAnalise(cliente, novoLimiteSolicitado);
    return (null, null, null, null, new LimiteSolicitado(idDaSolicitacao));
}

// para consumir...

[HttpPost("/clientes/{idCliente}/limite-de-credito/solicitar-aumento")]
public IActionResult SolicitarAumentoComTuplas(Guid idCliente, NovoLimiteSolicitado novoLimiteSolicitado)
{
    var processador = new ProcessadorDeLimiteDeCredito(_clienteRepository);
    var (clienteNaoEncontrado, clienteInativado, clienteEmAnalise, limiteSolicitadoExcedeLimiteMaximo, limiteSolicitado) = processador.SolicitarAumentoComTuplas(idCliente, novoLimiteSolicitado);

    if(clienteNaoEncontrado is not null)
        return Results.NotFound(clienteNaoEncontrado);

    if (clienteInativado is not null)
        return Results.BadRequest(clienteInativado);    

    if (clienteEmAnalise is not null)
        return Results.Accepted(clienteEmAnalise);

    if (limiteSolicitadoExcedeLimiteMaximo is not null)
        return Results.BadRequest(limiteSolicitadoExcedeLimiteMaximo);

    return Results.Created($"/clientes/{idCliente}/limite-de-credito/solicitacoes/{limiteSolicitado!.IdSolicitacao}");
}
Enter fullscreen mode Exit fullscreen mode

Se a quantidade de retornos for baixa, algo como dois ou três, a gente até consideraria essa abordagem. Podemos até discutir se faz sentido ou não uma tupla com esses valores já que eles não se relacionam e não têm nada em comum a não ser serem resultados de um processamento.

O ClienteNaoEncontrado não tem nenhuma relação forte com LimiteSolicitadoExcedeLimiteMaximo. Analisando por esse prisma, conceitualmente estamos utilizando tuplas de maneira equivocada.

Porém, além disso temos um problema ainda maior, que é o fato de que a manutenção se torna muito complexa.

Nós tiramos um pouco da carga cognitiva de quem consome o método já que os retornos são tipados e descritivos, eu não preciso saber como montar o resultado para a apresentação, porém, caso seja necessária a inclusão de mais uma situação, a manutenção do código se torna caótica.

Além do mais, aqueles nulos me causam dor de estômago...

Existem várias abordagens que tentam resolver esse problema.

Já vi inclusive uma na qual o retorno do método é um... object... Meu fígado dói só de pensar...

Mas, como a pessoa programadora é o ser mais criativo do universo observável, ao invés de retornar object, alguém deu uma ideia (que até aparenta ser interessante): Criar uma interface de marcação (interface vazia) onde todos os retornos a implementam, e o método a retorna. Agora temos um tipo específico de retorno.

Dessa maneira não é retornado um object, e sim uma interface e nosso método fica um pouco mais... digamos... elegante.

Parece ser uma boa ideia... vamos olhar com calma.

public interface IResultadoSolicitcaoAumento;
public record ClienteNaoEncontrado(Guid IdCliente) : IResultadoSolicitcaoAumento;  
public record ClienteInativado(Guid IdCliente, decimal ValorDevido) : IResultadoSolicitcaoAumento;  
public record ClienteEmAnalise(Guid IdCliente, DateTime DataDaSolicitacao) : IResultadoSolicitcaoAumento;  
public record LimiteSolicitadoExcedeLimiteMaximo(TipoCliente Tipo, decimal ValorSolicitado, decimal LimiteMaximo) : IResultadoSolicitcaoAumento;  
public record LimiteSolicitado(Guid IdSolicitacao) : IResultadoSolicitcaoAumento;  

public IResultadoSolicitcaoAumento SolicitarAumentoComInterface(Guid idDoCliente, NovoLimiteSolicitado novoLimiteSolicitado)
{
    var cliente = clienteRepository.ObterPorId(idDoCliente);
    if (cliente is null)
        return new ClienteNaoEncontrado(idDoCliente);

    if (cliente.EstaInativado)
        return new ClienteInativado(idDoCliente, cliente.ValorDevido);

    if (cliente.EmAnalise)
        return new ClienteEmAnalise(idDoCliente, cliente.DataDaSolicitacao.GetValueOrDefault());

    if (cliente.Tipo == TipoCliente.Comum && novoLimiteSolicitado.Valor > 1000)
        return new LimiteSolicitadoExcedeLimiteMaximo(cliente.Tipo, novoLimiteSolicitado.Valor, 1000);

    if (cliente.Tipo == TipoCliente.Especial && novoLimiteSolicitado.Valor > 1500)
        return new LimiteSolicitadoExcedeLimiteMaximo(cliente.Tipo, novoLimiteSolicitado.Valor, 1500);

    if (cliente.Tipo == TipoCliente.Premium && novoLimiteSolicitado.Valor > 2000)
        return new LimiteSolicitadoExcedeLimiteMaximo(cliente.Tipo, novoLimiteSolicitado.Valor, 2000);

    var idDaSolicitacao = EnviarParaAnalise(cliente, novoLimiteSolicitado);
    return new LimiteSolicitado(idDaSolicitacao);
}

// pra consumir...

[HttpPost("/clientes/{idCliente}/limite-de-credito/solicitar-aumento")]
public IActionResult SolicitarAumentoComInterface(Guid idCliente, NovoLimiteSolicitado novoLimiteSolicitado)
{
    var processador = new ProcessadorDeLimiteDeCredito(_clienteRepository);
    var resultado = processador.SolicitarAumentoComInterface(idCliente, novoLimiteSolicitado);

    if(resultado is ClienteNaoEncontrado clienteNaoEncontrado)
        return Results.NotFound(clienteNaoEncontrado);

    if (resultado is ClienteInativado clienteInativado)
        return Results.BadRequest(clienteInativado);    

    if (resultado is ClienteEmAnalise clienteEmAnalise)
        return Results.Accepted(clienteEmAnalise);

    if (resultado is LimiteSolicitadoExcedeLimiteMaximo limiteSolicitadoExcedeLimiteMaximo)
        return Results.BadRequest(limiteSolicitadoExcedeLimiteMaximo);

    if(resultado is LimiteSolicitado limiteSolicitado)
        return Results.Created($"/clientes/{idCliente}/limite-de-credito/solicitacoes/{limiteSolicitado!.IdSolicitacao}");

    throw new InvalidCastException(
        $"Não foi possível converter o resultado {resultado.GetType().Name} para um tipo válido.");
}
Enter fullscreen mode Exit fullscreen mode

Essa abordagem chega a ser interessante, apesar do retorno do método não descrever de fato o que ele retorna, afinal a interface é vazia, sua implementação é elegante. Para cada situação, temos uma estrutura definida, temos uma representação forte do retorno daquela condição em específico.

Porém, o consumo desse método me obriga a saber exatamente quais são seus tipos de retorno e isso abre uma margem grande para ocorrer erros, afinal, podemos adicionar um novo retorno e não o tratar dentro da nossa action já que não existe nada que me obrigue a fazer isso, a não ser uma exceção que é lançada avisando que o tipo retornado não foi mapeado.

Eu acho isso trágico...

Essa bateu na trave...

Tuplas, não rolou... Interface de marcação também não...

Os tipos criados me descrevem exatamente como cada situação deve ser tratada. E agora?

Pois é... que pena que o csharp não tenha suporte a Discriminated Unions.

Porém, podemos fazer um exercício de imaginação, criando um suporte a Discriminated Unions fictício...

O código abaixo não compila, é apenas a representação de uma ideia.

public record ClienteNaoEncontrado(Guid IdCliente);  
public record ClienteInativado(Guid IdCliente, decimal ValorDevido);  
public record ClienteEmAnalise(Guid IdCliente, DateTime DataDaSolicitacao);  
public record LimiteSolicitadoExcedeLimiteMaximo(TipoCliente Tipo, decimal ValorSolicitado, decimal LimiteMaximo); 
public record LimiteSolicitado(Guid IdSolicitacao);

public <ClienteNaoEncontrado | ClienteInativado | ClienteEmAnalise | LimiteSolicitadoExcedeLimiteMaximo | LimiteSolicitado> SolicitarAumentoDiscriminatedUnions(Guid idDoCliente, NovoLimiteSolicitado novoLimiteSolicitado)
{
    var cliente = clienteRepository.ObterPorId(idDoCliente);
    if (cliente is null)
        return new ClienteNaoEncontrado(idDoCliente);

    if (cliente.EstaInativado)
        return new ClienteInativado(idDoCliente, cliente.ValorDevido);

    if (cliente.EmAnalise)
        return new ClienteEmAnalise(idDoCliente, cliente.DataDaSolicitacao.GetValueOrDefault());

    if (cliente.Tipo == TipoCliente.Comum && novoLimiteSolicitado.Valor > 1000)
        return new LimiteSolicitadoExcedeLimiteMaximo(cliente.Tipo, novoLimiteSolicitado.Valor, 1000);

    if (cliente.Tipo == TipoCliente.Especial && novoLimiteSolicitado.Valor > 1500)
        return new LimiteSolicitadoExcedeLimiteMaximo(cliente.Tipo, novoLimiteSolicitado.Valor, 1500);

    if (cliente.Tipo == TipoCliente.Premium && novoLimiteSolicitado.Valor > 2000)
        return new LimiteSolicitadoExcedeLimiteMaximo(cliente.Tipo, novoLimiteSolicitado.Valor, 2000);

    var idDaSolicitacao = EnviarParaAnalise(cliente, novoLimiteSolicitado);
    return new LimiteSolicitado(idDaSolicitacao);
}

// pra consumir...

[HttpPost("/clientes/{idCliente}/limite-de-credito/solicitar-aumento")]
public IActionResult SolicitarAumentoDiscriminatedUnions(Guid idCliente, NovoLimiteSolicitado novoLimiteSolicitado)
{
    var processador = new ProcessadorDeLimiteDeCredito(_clienteRepository);  
    var resultado = processador.SolicitarAumentoDiscriminatedUnions(idCliente, novoLimiteSolicitado);  
    return resultado switch  
    {  
        ClienteNaoEncontrado => Results.NotFound(resultado),  
        ClienteInativado => Results.BadRequest(resultado),  
        ClienteEmAnalise => Results.Accepted(resultado),  
        LimiteSolicitadoExcedeLimiteMaximo => Results.BadRequest(resultado),  
        LimiteSolicitado => Results.Created(  
        $"/clientes/{idCliente}/limite-de-credito/solicitacoes/{resultado.IdSolicitacao}")  
    };
}
Enter fullscreen mode Exit fullscreen mode

Nessa abordagem fictícia, eu descrevo exatamente quais são os retornos possíveis do meu método <ClienteNaoEncontrado | ClienteInativado | ClienteEmAnalise | LimiteSolicitadoExcedeLimiteMaximo | LimiteSolicitado>. A IDE vai nos ajudar mostrando isso!

E o consumo utilizaria pattern matching, o que obrigaria a validação de todos os retornos. Sendo assim, se meu método adicionar mais um tipo de retorno, quem o consome vai ter a obrigação de efetuar sua validação.

O mais legal disso é que podemos ter inferência de dados: No caso de LimiteSolicitado => Results.Created(
$"/clientes/{idCliente}/limite-de-credito/solicitacoes/{resultado.IdSolicitacao}")
, a variável resultado seria fortemente tipada como LimiteSolicitado dando acesso a propriedade IdSolicitacao.

Isso seria muito útil. Mas muito útil mesmo!

Porém, ainda não temos suporte a isso :/

Ao olhar o repositório do csharp no github, podemos ver várias discussões e uma proposta de implementação: csharplang/proposals/discriminated-unions.md at main · dotnet/csharplang (github.com). Essa proposta utiliza o termo enum class, e eu gostei muito dessa nomenclatura.

Usando essa abordagem teríamos algo como...

enum class ResultadoSolicitcaoAumento
{
    ClienteNaoEncontrado(Guid IdCliente);  
    ClienteInativado(Guid IdCliente, decimal ValorDevido); 
    ClienteEmAnalise(Guid IdCliente, DateTime DataDaSolicitacao);  
    LimiteSolicitadoExcedeLimiteMaximo(TipoCliente Tipo, decimal ValorSolicitado, decimal LimiteMaximo); 
    LimiteSolicitado(Guid IdSolicitacao);
}

public ResultadoSolicitcaoAumento SolicitarAumentoDiscriminatedUnions(Guid idDoCliente, NovoLimiteSolicitado novoLimiteSolicitado)
{
    //...
}

// o consumo continuaria da mesma forma...
Enter fullscreen mode Exit fullscreen mode

Acredito que essa abordagem ficaria ainda mais elegante!

Só que até agora, nada :( Não se sabe quando vão suportar Discriminated Unions. Eu acredito que vão implementar. Só não sei quando :/

Inclusive, para suportar esse tipo de funcionalidade, foi criada a biblioteca OneOf: mcintyre321/OneOf: Easy to use F#-like ~discriminated~ unions for C# with exhaustive compile time matching (github.com)

Esta biblioteca fornece o Discriminated Unions no estilo F# para C#, usando um tipo personalizado OneOf. Uma instância desse tipo contém um único valor, que é um dos tipos em sua lista genérica de argumentos.

Com essa biblioteca, o uso dessa abordagem se torna um pouco mais simples, como podemos ver no exemplo que eles apresentam no repositório oficial:

public OneOf<User, InvalidName, NameTaken> CreateUser(string username)
{
    if (!IsValid(username)) return new InvalidName();
    var user = _repo.FindByUsername(username);
    if(user != null) return new NameTaken();
    var user = new User(username);
    _repo.Save(user);
    return user;
}

[HttpPost]
public IActionResult Register(string username)
{
    OneOf<User, InvalidName, NameTaken> createUserResult = CreateUser(username);
    return createUserResult.Match(
        user => new RedirectResult("/dashboard"),
        invalidName => {
            ModelState.AddModelError(nameof(username), $"Sorry, that is not a valid username.");
            return View("Register");
        },
        nameTaken => {
            ModelState.AddModelError(nameof(username), "Sorry, that name is already in use.");
            return View("Register");
        }
    );
}
Enter fullscreen mode Exit fullscreen mode

Basicamente temos a mesma necessidade apresentada no exemplo acima, só que nesse caso, o retorno é feito como OneOf<User, InvalidName, NameTaken>, onde descrevemos quais tipos o método CreateUser pode retornar.

O objeto createUserResult tem um método chamado Match onde cada tipo de retorno tem um delegate e, sendo assim, é possível cada tipo ter sua implementação própria de retorno.

Funciona? Sim. É muito verboso? Sim. Mas é o que tem pra hoje!

Um canal que eu acho sensacional é o do Zoran Horvat. Esse cara é realmente incrível.

Ele fez um vídeo muito bacana sobre o assunto, mostrando uma implementação: Possibility of Discriminated Unions in C#: Functional Programming in .NET (youtube.com). Vale muito a pena assistir!


Chegamos ao fim do post.

E ae, o que achou? É muita viagem isso? Já caiu nesse cenário? Como que você resolveu?
Deixe seu comentário. Vai ser um baita prazer discutir com você esse assunto!

Até a próxima e bebam água.

Top comments (2)

Collapse
 
andrebaltieri profile image
Andre Baltieri

parabens pelo conteudo… você é fera.

Collapse
 
angelobelchior profile image
Angelo Belchior

Valew Balta :)