Design: Mônadas

William Santos - Apr 15 - - Dev Community

Olá! Este é mais um post da seção Design e nele trataremos de um recurso excelente oriundo das linguagens funcionais e que tem sido cada vez mais incentivado nas linguagens orientadas a objetos: mônadas!

Não pretendo explicar mônadas a partir da teoria das categorias, apenas explorar sua natureza conforme vemos na programação funcional.

O que é uma mônada?

Uma mônada é um invólucro (wrapper) para um tipo genérico que permite transformações neste tipo sempre retornando o invólucro.

Confuso? Explico.

No .NET temos os nullable types. Quando queremos admitir o valor null em um value type lançamos mão de algo como o que vemos abaixo:

int? x = null;
Enter fullscreen mode Exit fullscreen mode

Simples. Certo?

Agora, quando vamos a fundo no que significa o código acima, descobrimos um tipo chamado Nullable<T>, que representa o seguinte:

Nullable<int> x = null;
Console.WriteLine(x.HasValue);
//false
...
int? x = 10;
int? y = 20;
Console.WriteLine(x + y); //Equivale a x.Value + y.Value
//30
Enter fullscreen mode Exit fullscreen mode

Ou seja, temos um tipo que carrega, no nosso caso, um valor do tipo int e permite que sejam feitas operações com ele como se com um int fosse, admitindo null no processo, sempre retornando um Nullable.

Isso significa que Nullable<T> é uma mônada? Na verdade não. Vamos explorar um tanto mais este ponto.

Mais que um invólucro

Apesar de ser uma característica fundamental para uma mônada, ser um invólucro não a descreve totalmente. Além de permitir que um dado tipo seja envolvido uma mônada precisa ser capaz de realizar transformações e, para isso, é necessário uma função (método) de transformação (Bind).

Vamos usar como exemplo o tipo mais conhecido e utilizado, o Option (conhecido como Maybe em outras linguagens). Veja o código abaixo:

var value = obj.Method() //retorna 50
               .ToOption<int>()
               .Bind((v) => r > 0 ? "Maior que zero!" : "Menor ou igual a zero!")
               .Contains("Maior que zero!");
Console.WriteLine(value);
//true
Enter fullscreen mode Exit fullscreen mode

Repare que aqui temos uma função Bind que opera sobre Option<int>, o transforma em um Option<string> e, por fim, ao ser encadeado com Contains (que verifica se o valor envolvido é o passado como parâmetro) nos retorna um bool.

Embora a função Bind seja um requisito para definir uma mônada é possível ter diversas outras funções que manipulem o valor envolvido por ela para simplificar seu uso.

Flattening (achatamento)

Mark Seeman , em sua série sobre mônadas (e functors, mas estes estão fora do escopo deste post) argumenta que uma mônada é um tipo "achatável".

Isso significa que toda mônada deveria ser capaz de retornar uma mônada envolvida por ela por meio de uma função de achatamento (Flatten).

Vamos a mais um exemplo, simples para ilustração, também com Option<T>:

var option = Option.Some<Option<int>>(obj.Method()); //retorna Option<int> (10)
Console.Write(option.Flatten());
//10
Enter fullscreen mode Exit fullscreen mode

O código acima representa um Option<T> que pode receber outro a partir da execução de um método e, caso isso ocorra, pode obter a Option<T> envolvida via Flatten, permitindo acesso ao valor por este envolvido, neste caso 10.

Bons exemplos

Agora que entendemos um tanto sobre a natureza das mônadas, vamos conhecer as que já estão disponíveis no F#. Spoiler: as portei para o C# por meio do projeto Moonad, e será dele o código que veremos a seguir.

Choice

Este tipo, também conhecido como Either em outras linguagens, nos permite escolher um entre dois (ou mais) tipos para retornar de um método. Talvez soe estranho em um primeiro momento mas considere o seguinte cenário: um usuário pode escolher fazer seu login utilizando email ou CPF. Qualquer que seja sua escolha, uma vez verificada sua existência na base de usuário, ele será direcionado para o pedido de senha.

Vejamos como o Choice<T1, T2, T3> pode nos ajudar nisso.

public Option<User> GetUser(string input)
{
    Choice<Email, Cpf, InvalidKey> key = ExtractKey(input);
    return key switch
    {
        Email => users.GetByEmail(key),
        Cpf => users.GetByCpf(key),
        InvalidKey => Option.None<User>()
    }
}
Enter fullscreen mode Exit fullscreen mode

Veja que o método ExtractKey vai tentar converter a entrada para um dos dois tipos válidos de identificador e, caso não consiga, retorna um tipo que expressa uma chave inválida. E, a partir da chave, tenta obter um usuário correspondente. Você pode ter notado que, além de usar Choice para retornar a chave, também é usado Option para retornar o usuário.

Nota: ao contrários das demais mônadas, que possuem uma função de achatamento, Choice não o tem. Isso significa que Choice não é uma mônada? De forma alguma. A função Flatten é substituída pela conversão implícita do valor envolvido por Choice. Ou seja, caso o valor envolvido por Choice<int, string> seja um Choice<int>, haverá uma conversão implícita de Choice<int> para int, tendo o exato mesmo efeito esperado da função Flatten.

Result e Result<TResult, TError>

Este tipo representa o resultado de uma operação. É usualmente confundido com Choice<T1, T2> pelo fato de ambos poderem retornar um de dois tipos, mas Result possui indicadores próprios que indicam se uma operação ocorreu ou não com sucesso (IsOk), algo que, com Choice teria de ser verificado via pattern matching.

Result é indicado para métodos que envolvem processamentos, normalmente operações de cálculo, transformação de valores ou escrita em mecanismo de persistência/mensageria. Caso a operação ocorra sem intercorrências e haja algum tipo específico a se retornar (como, por exemplo, o número de registros afetados por uma instrução SQL), usa-se Result<TResult, TError>, do contrário apenas Result é necessário.

Um detalhe interessante é que Result pode ser usado para evitar que exceções sejam lançadas em métodos que normalmente seriam do tipo void. Ou seja, caso uma exceção seja lançada em um dado método como, por exemplo, uma tentativa de escrita em banco de dados, esta pode ser substituída por uma instância de Result que represente apenas que a operação não pôde ser concluída, deixando os detalhes da manipulação da exceção apenas a cargo do objeto de acesso a este banco.
Result<TResult,TError> por sua vez pode ser usado para retornar tipos de erro que sejam semanticamente significativos para o domínio da aplicação, evitando assim as chamadas business exceptions.

Vamos aos exemplos para deixar mais claro.

Exemplo 1 - tentativa de criação de um usuário na base de dados:

public Result PersistUser(User)
{
    try
    {
        //SQL...
        return true;
    }
    catch(Exception exc)
    {
        //Logs...
        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

Exemplo 2 - tentativa de criação de um usuário:

public interface IUserError {}
public struct AlreadyExistingEmailError : IUserError {}
public struct AleradyExistingCpfError : IUserError {}
...
public Result<User, IUserError> Create(Email email, Cpf cpf, Password password)
{
    //Email já existe
    return new AlreadyExistingEmailError();
    //Cpf já existe
    return new AlreadyExistingCpfError();
}
...
public IActionResult Create(CreateUserRequest request)
{
    var result = UserService.Create(email, cpf, password);
    if(result)
        return Created(string.Empty, result);

    var conflict = result switch
    {
        AlreadyExistingEmailError => "E-mail já cadastrado.",
        AlreadyExistingCpfError => "Cpf já cadastrado."
    }
    return Conflict(conflict);
}

Enter fullscreen mode Exit fullscreen mode

Nota: veja que interessante: no segundo exemplo utilizamos structs cujos tipos bastam para compreender a natureza do erro, deixando a mensagem amigável ao usuário na borda do sistema, onde a comunicação de fato acontece.
Essa é uma ótima prática para evitar que mensagens ao usuário sejam escritas em objetos que pertençam ao domínio da aplicação, onde preocupações com a apresentação ao usuário não deveriam existir, o que acabaria por criar um acoplamento desnecessário.

Option

Option<T> é um tipo bem peculiar de mônada por representar a existência ou ausência de um valor. Quando há um valor envolvido por Option<T> ele se manifesta como um Some e, quando não há, como um None.
Isso permite a Option<T> previnir o velho problema do NullReferenceException, e isso é algo que considero essencial para evitar que nosso próprio código gere essas exceções. Isso é tão importante que a Microsoft nos brindou com o Nullable Reference Types para facilitar a identificação, restrição e permissão de potenciais valores nulos em nossas aplicações.

Vamos ver na prática como isso funciona. Imagine que você esteja lidando com uma biblioteca de terceiros onde um dado objeto pode retornar nulo em um de seus métodos, um int? por exemplo. Para eliminar a preocupação com este valor nulo, e poder retornar sem preocupações, o seguinte código pode ser implementado:

public Option<T> MyMethod(Whatever obj)
{
    Option<int> value = obj.Method().ToOption(); //retorna null
    //value é Option<int>.None
    return value;
}
Enter fullscreen mode Exit fullscreen mode
public Option<T> MyMethod(Whatever obj)
{
    Option<int> value = obj.Method().ToOption(); //retorna 10
    //value é Option<int>.Some (10)
    return value;
}
Enter fullscreen mode Exit fullscreen mode

É exatamente essa característica que permite, como visto no começo do post, realizarmos diversas operações. Muitas delas, constantes do próprio tipo Option<T>, que verificam por si a presença/ausência de valor e retornam de acordo, dispensando assim a necessidade de verificar a todo momento se um dado valor é ou não nulo.

Nota: existe uma versão de Option<T> específica para value types, chamada ValueOption<T>. Ela é, também, um value type e deve ser usada em cenários onde performance seja crítica para a aplicação.

Considerações Finais

Este foi um post introdutório sobre mônadas, com um resumo do seu conceito e aplicação. Caso queira se aprofundar no assunto, recomendo este ótimo livro do Enrico Buonanno, que serviu de referência para este post.

Alguém disse "código"?

Aqui temos o repositório do Moonad no GitHub. Fique à vontade para estudá-lo e, para testá-lo, pode baixar o pacote Nuget. Lembrando que a documentação está disponível em moonad.net.

Gostou? Me deixe saber pelos indicadores. Fique à vontade para deixar um comentário ou me procurar em minhas redes sociais.

Ah! E caso queira mais conteúdo sobre mônadas, outros tipos (functors e aplications) e programação funcional em geral,me deixe saber que posto assim que possível.

Até a próxima!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .