
Compartilhar:
Guillaume é um ex-membro da equipe da Vonage. Ele atuou como Promotor Sênior de Desenvolvimento .NET na Vonage. Trabalha com .NET há quase 15 anos, tendo se dedicado, nos últimos anos, à promoção do Software Craftsmanship. Seus temas favoritos incluem qualidade de código, automação de testes, mobbing e code katas. Fora do trabalho, ele gosta de passar tempo com a esposa e a filha, malhar ou jogar videogame.
A Invasão das Monads – Parte 2: Monads em ação!
Tempo de leitura: 8 minutos
Olá, amigos,
Em nosso post anterior, “A Invasão das Monads – Parte 1: O que é uma Monad?”, optamos por uma abordagem pragmática para apresentar as monades, construindo a nossa própria de forma gradual. A esta altura, você já deve estar familiarizado com o conceito, sabendo como transformar seu valor usando .Map() ou .Bind() e como extraí-lo com .Match().
Neste artigo, queremos demonstrar aplicações práticas para várias mónadas e, para isso, usaremos um exemplo do SDK do Vonage para .NET. Mas, primeiro, vamos fazer uma rápida recapitulação.
Resumo rápido
Anteriormente, criamos a Optional Monad, que pode existir em um dos dois estados: Some, indicando a presença de um valor, ou None, indicando a ausência de valor.
private static SchrodingerBox<int> Half(int value) =>
value % 2 == 0
? SchrodingerBox<int>.Some(value / 2)
: SchrodingerBox<int>.None();
private static int Increment(int value) => value + 1;
SchrodingerBox<int>.Some(0)
.Map(Increment) // Some(0) becomes Some(1)
.Map(Increment) // Some(1) becomes Some(2)
.Bind(Half) // Some(2) becomes Some(1)
.Map(Increment) // Some(1) becomes Some(2)
.Match(some => $"The value is some {some}!", () => "The value is none")
.Should()
.Be("The value is some 2!");
SchrodingerBox<int>.Some(0)
.Map(Increment) // Some(0) becomes Some(1)
.Bind(Half) // Some(1) becomes None - the Monad's state has changed
.Map(Increment) // None remains None
.Match(some => $"The value is some {some}!", () => "The value is none")
.Should()
.Be("The value is none");
Nosso aparelho oferece três funcionalidades essenciais:
.Map(operation)permite que você gere um novo valor com base em um já existente e coloque o novo valor em uma nova caixa. A operação só é executada se a caixa estiver noSomeestado..Bind(operation)é um mecanismo de transformação semelhante a.Map(). No entanto, ele difere por permitir a alteração do estado da caixa, já que a função retorna uma caixa em vez de um valor. Assim como.Map(), a operação só é executada se a caixa estiver noSomeestado..Match(some, none)avalia o estado da caixa e chama a função correspondente.
A caixa permite que você altere um valor sem saber qual é o seu estado atual.
Em essência, o estado se torna irrelevante, já que a sequência de operações permanece a mesma. Como você provavelmente observou, o código não possui construções de ramificação (if/else).
Quando vale a pena usar uma mônada?
An image for exposing two different states
Devido à sua natureza de estados opostos, as monades revelam-se irrelevantes em cenários em que as operações produzem um único resultado. Para que as monades sejam aplicáveis, a operação deve apresentar pelo menos dois resultados possíveis distintos.
Até agora, trabalhamos com a Optional mônada, mas existem muitas outras. Para compartilhar exemplos de outras mônadas, vamos explorar quais mônadas a biblioteca Language-Ext oferece e descobrir em que contextos elas podem ser úteis.
Opção
Considerando nossa implementação anterior, você já deve estar bastante familiarizado com essa função. Ela se destaca ao envolver um valor opcional ou um valor que pode (ou não) existir.
private async Task<Option<User>> FindUser(Guid id)
{
var user = await this.repository.Users.FirstOrDefaultAsync(user => user.Id == id);
return user ?? Option<User>.None;
}
Experimente
O Try<T> Monad representa uma operação que pode falhar:
Successindica que a operação foi bem-sucedida, gerando o resultado<T>.Exceptionindica que a operação resultou em uma exceção. A Monad retornará a exceção em vez de lançá-la.
private static Try<decimal> Divide(decimal value, decimal divisor) =>
Try(() => value / divisor);
No exemplo acima, podemos encapsular uma operação potencialmente “arriscada” dentro de um Try.
Ao chamar
Divide(50,2)retornará umSuccessestado com o valor25.Ao chamar
Divide(50,0)retornará umExceptionestado com umDivideByZeroException.
Ou
A Either<L, R> Monad representa uma operação que pode retornar dois tipos distintos, definidos por Leftou Right. Essa Monad é extremamente versátil, já que <L, R> são ambos tipos genéricos. No entanto, o Left estado geralmente representa erros ou casos excepcionais.
private static Either<Error, decimal> Divide(decimal value, decimal divisor) =>
divisor == 0
? new Error("Cannot divide by 0.")
: value / divisor;
private record Error(string Message);
Seguindo o cenário anterior:
Ao chamar
Divide(50,2)retornará umRightestado com o valor25.Ao chamar
Divide(50,0)retornará umLeftestado com umErrorregistro explicando por que ele não foi processado.
Validação
O Validation<Fail, Success> Monad representa uma operação de validação que pode falhar por diversos motivos.
private record User(string Firstname, string Lastname, MailAddress Email);
private static Validation<string, User> CreateUser(string firstname, string lastname, string email)
{
var errors = new Seq<string>();
if (string.IsNullOrWhiteSpace(firstname))
{
errors = errors.Add("Firstname cannot be empty.");
}
if (string.IsNullOrWhiteSpace(lastname))
{
errors = errors.Add("Lastname cannot be empty.");
}
if (!MailAddress.TryCreate(email, out var address))
{
errors = errors.Add("Invalid mail address.");
}
return errors.Any()
? Validation<string, User>.Fail(errors)
: Validation<string, User>.Success(new User(firstname, lastname, address));
}
Neste exemplo:
Ao chamar
CreateUser("Jane", "Doe", "jane.doe@email.com")retornará umSuccesscom umaUserinstância.A chamada de
CreateUser(null, null, null)(ou qualquer valor inválido) retornará umaFailurecom a coleção de erros encontrados.
Transparência referencial
An image to illustrate transparency
Todas essas Monads têm algo em comum: são todas transparentes quanto aos resultados potenciais de uma operação, seja ela uma falha, o lançamento de uma exceção ou o retorno de vários valores. Assim, o tipo de retorno do método deve comunicar o resultado desses desfechos, em vez de um mecanismo invisível e imprevisível (uma exceção).
Aqui, estamos falando sobre Transparência Referencial. Ela se aplica quando podemos sempre substituir uma expressão por seu valor.
// Referentially Opaque examples
public User FindUser(Guid id) => this.users.First(user => user.Id == id);
public int Divide(int a, int b) => a/b;
public void SetName(string name)
{
ArgumentNullException.ThrowIfNull(name);
this.Name = name;
}
// Referentially Transparent examples
public Maybe<User> FindUser(Guid id) => this.users.FirstOrDefault(user => user.Id == id) ?? Maybe<User>.None;
public Try<decimal> Divide(decimal value, decimal divisor) => Try(() => value / divisor);
public int Add(int a, int b) => a+b;
public Either<Error, Unit> SetName(string name)
{
if (name is null)
{
return new Error("Name cannot be null);
}
this.Name = name;
return Unit.Value;
}
Você provavelmente percebeu que
.SetName()retorna umEither<Error, Unit>. Você talvez já tenha se deparado comUnitem bibliotecas como MediatR ou Language-Ext. É uma construção simples que representa um tipo com apenas um valor possível. Nós a usamos como um substituto para operações que não retornam um valor, mas podem retornar outro estado. No nosso exemplo,.SetName()é um Command que não retorna um valor, mas pode falhar. Portanto, a mônadaEither<Error, Unit>possui dois estados possíveis: Right (sem valor) ou Left (com um Error).
Embora a Transparência Referencial não resolva, por si só, problemas específicos, ela melhora significativamente a previsibilidade e a legibilidade do código. Tornar tudo explícito na assinatura do método minimiza a possibilidade de ocorrências inesperadas. O Tio Bob enfatiza em seu livro “Clean Code: A Handbook of Agile Software Craftsmanship” que “a proporção entre o tempo gasto lendo e escrevendo é bem superior a 10 para 1. Estamos constantemente lendo código antigo como parte do esforço para escrever código novo... Portanto, facilitar a leitura facilita a escrita”. Isso destaca a importância da clareza e da transparência do código para facilitar tanto a compreensão quanto o desenvolvimento eficiente.
Essa foi minha principal motivação para a criação de “Jogue as exceções... para fora do seu código".
Exemplo do SDK da Vonage
Até agora, nos concentramos em exemplos relativamente simples. No entanto, e quanto ao uso de monads em fluxos mais complexos? Vamos dar uma olhada em um exemplo do nosso SDK do .NET que envolve nossa API de autenticação de dois fatores Verify.
Monads personalizadas
Anteriormente, criamos um Optional e mostramos um conjunto existente baseado em Language-Ext, mas várias bibliotecas também oferecem implementações de Monad. No caso do nosso SDK, evitamos deliberadamente depender de bibliotecas externas como Language-Ext. De fato, as Monads fazem parte da API pública do SDK, e depender de uma biblioteca externa introduziria uma dependência sobre a qual teríamos controle limitado.
Nossa abordagem consistiu em criar nossas próprias implementações personalizadas de Monads, adaptadas às necessidades específicas do SDK. Essa estratégia nos permitiu manter o controle sobre o design e a funcionalidade dessas Monads, evitando, ao mesmo tempo, dependências de bibliotecas externas.
Além disso, nosso objetivo era apresentar uma versão leve e fácil de usar de certas Monads, garantindo uma adoção mais fácil por parte dos desenvolvedores que trabalham com nosso SDK.
Nosso SDK implementa as seguintes monads:
Result<T>Monad, semelhante a umEither<IFailure, T>com uma sintaxe mais concisa — já que o C# é prolixo no que diz respeito a genéricos.Maybe<T>Monada, semelhante a umaOption<T>.
Exemplo com autenticação de dois fatores
A autenticação de duas etapas (2FA) é um fluxo de trabalho em duas etapas. Primeiro, iniciamos um processo de autenticação, no qual o cliente recebe um código de validação de acordo com o fluxo de trabalho especificado (SMS, WhatsApp, e-mail, mensagem de voz, SilentAuth). Assim que o código é recebido, nós o enviamos à nossa API para verificação.
var myPhoneNumber = ...
var result = await StartVerificationRequest.Build()
.WithBrand("Monads Inc.")
.WithWorkflow(SmsWorkflow.Parse(myPhoneNumber))
.Create() // Build the authentication request
.BindAsync(request => verifyClient.StartVerificationAsync(request)) // Process the authentication request
.BindAsync(VerifyCodeAsync) // Start the second step if it's a success
.Match(AuthenticationSuccessful, AuthenticationFailed);
private async Task<Result<Unit>> VerifyCodeAsync(StartVerificationResponse response)
{
var myCode = ... // Receive verification code based on the specified workflow
return await response
.BuildVerificationRequest(myCode) // Build the verification request
.BindAsync(request => verifyClient.VerifyCodeAsync(request)); // Process the verification request
}
Tudo o que abordamos até agora continua válido, já que nosso fluxo de trabalho envolve a obtenção de entradas do usuário, a análise de operações e a realização de chamadas de API.
Mais uma vez, conseguimos eliminar as ramificações do nosso código, mantendo um fluxo consistente independentemente do estado da Monad. Vale a pena observar que nosso processo pode falhar em cinco pontos distintos:
Ao criar a solicitação de autenticação
Ao processar a solicitação de autenticação
Ao criar a solicitação de verificação
Ao processar a solicitação de verificação
Ao chamar a segunda etapa
VerifyCodeAsync
Se você se lembra, em nosso post anterior, falamos sobre manter uma Monad ativa pelo maior tempo possível. Nesse cenário, o valor permanece em nossa Result<T> desde o início até o fim do fluxo, permitindo-nos encadear toda a sequência de operações.
Ser ou não ser... puro?
An image to illustrate purity
No exemplo anterior, você deve ter percebido que nossa Monad não permaneceu pura durante o fluxo de trabalho. De fato, fornecemos funções impuras para .Bind(), como request => verifyClient.StartVerificationAsync(request) ou request => verifyClient.VerifyCodeAsync(request). Mas isso seria um problema?
Por definição, um função função não se refere a nenhum estado global e deve não produzir nenhum efeito colateral. Ela produz consistentemente um resultado que depende exclusivamente da entrada, garantindo o mesmo resultado para uma entrada específica — em outras palavras, alta previsibilidade.
Embora o conceito de “mônada pura” possa ser discutido, é essencial compreender que o uso de mônadas não exige necessariamente que a própria Monad seja pura. Em vez disso, as mônadas são frequentemente usadas para estruturar cálculos que envolvem operações impuras , ao mesmo tempo em que se mantêm os princípios da programação funcional. De fato, interações com recursos externos, como bancos de dados ou APIs, introduzem efeitos colaterais.
Monades impuras... com exceções?
O comportamento padrão garante que nossas mônadas não lancem nenhuma exceção, mesmo que a função parâmetro dentro delas o faça. Essa escolha de design nos ajuda a manter referential transparency.
public Result<TB> Bind<TB>(Func<T, Result<TB>> bind)
{
try
{
return this.IsFailure
? Result<TB>.FromFailure(this.failure)
: bind(this.success);
}
catch (Exception exception)
{
return SystemFailure.FromException(exception).ToResult<TB>();
}
}
public Result<TB> Map<TB>(Func<T, TB> map)
{
try
{
return this.IsFailure
? Result<TB>.FromFailure(this.failure)
: Result<TB>.FromSuccess(map(this.success));
}
catch (Exception exception)
{
return SystemFailure.FromException(exception).ToResult<TB>();
}
}
No entanto, e se você preferir continuar usando exceções em vez de extrair o valor com .Match()? Queríamos que nossas monades fossem versáteis, por isso introduzimos uma GetSuccessUnsafe() funcionalidade. Essa função lançará uma exceção se a mônada estiver no Failure estado.
public T GetSuccessUnsafe() => this.IfFailure(value => throw value.ToException());
O tipo e os dados da exceção dependem do valor da falha subjacente:
ResultFailurevai causar umVonageExceptionParsingFailurevai causar umVonageExceptionHttpFailurevai causar umVonageHttpRequestExceptionAuthenticationFailurevai causar umVonageAuthenticationExceptionE assim por diante...
Veja a seguir como você pode incorporar exceções ao seu fluxo monádico usando o mesmo exemplo de antes:
try
{
await StartVerificationRequest.Build()
.WithBrand("Monads Inc.")
.WithWorkflow(SmsWorkflow.Parse(myPhoneNumber))
.Create()
.BindAsync(request => verifyClient.StartVerificationAsync(request))
.BindAsync(VerifyCodeAsync)
.GetSuccessUnsafe(); // Will throw exception if in the Failure state
AuthenticationSuccessful();
}
catch (Exception exception)
{
AuthenticationFailed(exception);
}
Quer você escolha a abordagem monádica padrão ou opte por exceções, nosso objetivo é acomodar diferentes estilos de tratamento de erros, garantindo que nossas monades se alinhem às suas preferências de programação.
Conclusão
Concluímos a segunda postagem da nossa série “A Invasão das Monads”. Este artigo teve como objetivo apresentar vários conjuntos de Monads, incluindo nossas implementações personalizadas, e ilustrar seu uso em fluxos de trabalho mais abrangentes.
A esta altura, você já deve ter percebido que o Monads oferece uma abordagem alternativa para o tratamento de erros em seus fluxos de trabalho — e isso não é por acaso. De fato, nossa próxima publicação irá esclarecer melhor a metodologia por trás do encadeamento de operações. Fique ligado para saber mais!
Se você tiver alguma dúvida ou quiser bater um papo, fique à vontade para me mandar uma mensagem no meu LinkedIn ou junte-se a nós no Slack dos desenvolvedores da Vonage.
Boa programação, e até mais tarde!
Compartilhar:
Guillaume é um ex-membro da equipe da Vonage. Ele atuou como Promotor Sênior de Desenvolvimento .NET na Vonage. Trabalha com .NET há quase 15 anos, tendo se dedicado, nos últimos anos, à promoção do Software Craftsmanship. Seus temas favoritos incluem qualidade de código, automação de testes, mobbing e code katas. Fora do trabalho, ele gosta de passar tempo com a esposa e a filha, malhar ou jogar videogame.