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;
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
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
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
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>()
}
}
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 queChoice
não é uma mônada? De forma alguma. A funçãoFlatten
é substituída pela conversão implícita do valor envolvido porChoice
. Ou seja, caso o valor envolvido porChoice<int, string>
seja umChoice<int>
, haverá uma conversão implícita deChoice<int>
paraint
, tendo o exato mesmo efeito esperado da funçãoFlatten
.
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.
Já 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;
}
}
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);
}
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;
}
public Option<T> MyMethod(Whatever obj)
{
Option<int> value = obj.Method().ToOption(); //retorna 10
//value é Option<int>.Some (10)
return value;
}
É 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, chamadaValueOption<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!