Separando os momentos de "planejamento do fluxo" e de execução, com programação funcional, em C#

Por 9 fevereiro, 2017CSharp, Programação

Tempo de leitura: 3 minutos

Há algum tempo venho adotando técnicas de programação funcional para garantir mais estabilidade em tempo de execução em meu código. Aos poucos, estou transferindo algumas implementações para um repositório no Github.

Em posts anteriores, mostrei como melhorar a manutenção do código melhorando a assinatura dos métodos. Recomendo que você leia esses posts antes de avançar.

Leu? Beleza. Considere o seguinte código:

using System;
using ElemarJR.FunctionalCSharp;

namespace Playground
{
    class Program
    {
        static DbContext dbContext = new DbContext();
        static void Main(string[] args)
        {
            var tryEmployee = Try
                .Run(AskEmployeeId)
                .Bind(ParseId)
                .Bind(GetEmployeeById);

            tryEmployee.Match(
                failure: exception => Console.WriteLine($"Failed to get employee - {exception}"),
                success: employee => Console.WriteLine($"{employee.Id} - {employee.Name}")
            );
        }

        public static string AskEmployeeId()
            => Console.ReadLine();

        public static Try<Exception, int> ParseId(string s)
            => Try.Run(() => int.Parse(s));

        public static Try<Exception, Employee> GetEmployeeById(int id)
            => Try.Run(() => dbContext.Find(id));

    }
}

Nesse código, solicitamos um Id para o usuário, interpretamos este Id como um inteiro para, finalmente, ir ao banco de dados.

Repare que as assinaturas de todos os métodos indica que o resultado poderá ser sucesso ou então uma exception. Repare que a adoção do tipo Try (veja o repositório no Github para implementação) força o "programador cliente" a fazer o tratamento adequado.

Algo que me agrada bastante nesta abordagem é que fica claro o fluxo de processamento que será executado (AskEmployeeId, ParseId, GetEmployeeById)

Pensando em versões Lazy

O planejamento do fluxo de execução não deveria implicar em sua execução. Não é o que acontece acima! Para que possamos separar o planejamento da execução, precisamos preparar nossos métodos para que eles possam ser executados de forma Lazy.

Antes de avançar, vamos definir alguns elementos novos em nossa biblioteca funcional.

using System;

namespace ElemarJR.FunctionalCSharp
{
    public delegate Try<Exception, TResult> PromiseOfTry<TResult>();

    public static class PromiseOfTry
    {
        public static Try<Exception, TResult> Run<TResult>(this PromiseOfTry<TResult> promise)
        {
            try
            {
                return @promise();
            }
            catch (Exception e)
            {
                return e;
            }
        }

        public static PromiseOfTry<TResult> Map<TInput, TResult>(
            this PromiseOfTry<TInput> promise,
            Func<TInput, TResult> map
        ) => () => promise.Run()
            .Match<Try<Exception, TResult>>(
                failure: exception => exception,
                success: r => map(r)
            );

        public static PromiseOfTry<TResult> Bind<TInput, TResult>(
            this PromiseOfTry<TInput> promise,
            Func<TInput, PromiseOfTry<TResult>> f
        ) => () => promise.Run().Match(
            failure: ex => ex,
            success: t => f(t).Run()
        );
    }
}

A definição acima permite que escrevamos métodos que posterguem a execução do código.

Voltemos ao nosso exemplo, aplicando essa técnica.

using System;
using ElemarJR.FunctionalCSharp;

namespace Playground
{
    class Program
    {
        static DbContext dbContext = new DbContext();
        static void Main(string[] args)
        {
            // planejamento do fluxo.
            var promise = 
                AskEmployeeId()
                .Bind(ParseId)
                .Bind(GetEmployeeById);

            // execução.
            var result = promise.Run();

            // resultado.
            result.Match(
                failure: exception => Console.WriteLine($"Failed to get employee - {exception}"),
                success: employee => Console.WriteLine($"{employee.Id} - {employee.Name}")
            );
        }

        public static PromiseOfTry<string> AskEmployeeId()
            => () => Console.ReadLine();

        public static PromiseOfTry<int> ParseId(string s) =>
            () => int.Parse(s);

        public static PromiseOfTry<Employee> GetEmployeeById(int id)
            => () => dbContext.Find(id);

    }
}

O que fizemos aqui foi retornar uma funcão capaz de executar a tarefa (uma promessa). O operador funcional Bind permite que as diversas promessas sejam combinadas autorizando a definição do fluxo.

Agora, a execução ocorrerá somente quando o operador Run for executado.

LINQ!

Podemos mudar a forma como definimos o fluxo através de alguns operadores exigidos pelo LINQ.

namespace System.Linq
{
    using ElemarJR.FunctionalCSharp;
        
    public static partial class LinqExtensions
    {
        public static PromiseOfTry<TResult> Select<TInput, TResult>(
            this PromiseOfTry<TInput> promise,
            Func<TInput, TResult> selector
        ) => promise.Map(selector);

        public static PromiseOfTry<NewTResult> SelectMany<TInput, TResult, NewTResult>(
            this PromiseOfTry<TInput> promise,
            Func<TInput, PromiseOfTry<TResult>> binder,
            Func<TInput, TResult, NewTResult> projector
        ) => () => promise.Run().Match(
            failure: exception => exception,
            success: obj => binder(obj).Run()
                .Match<Try<Exception, NewTResult>>(
                    failure: ex => ex,
                    success: o => projector(obj, o)
                )
        );
    }
}

Se você não entende esse código, não se assuste. Mas, garanto que você vai se beneficiar muito se conseguir aprender como Linq funciona.

Esses métodos nos permitem escrever o código assim:

using System;
using System.Linq;
using ElemarJR.FunctionalCSharp;

namespace Playground
{
    class Program
    {
        static DbContext dbContext = new DbContext();
        static void Main(string[] args)
        {
            // planejamento do fluxo.
            var promise =
                from userInput in AskEmployeeId()
                from parsedInput in ParseId(userInput)
                from user in GetEmployeeById(parsedInput)
                select user;
                
            // execução.
            var result = promise.Run();

            // resultado.
            result.Match(
                failure: exception => Console.WriteLine($"Failed to get employee - {exception}"),
                success: employee => Console.WriteLine($"{employee.Id} - {employee.Name}")
            );
        }

        public static PromiseOfTry<string> AskEmployeeId()
            => () => Console.ReadLine();

        public static PromiseOfTry<int> ParseId(string s) =>
            () => int.Parse(s);

        public static PromiseOfTry<Employee> GetEmployeeById(int id)
            => () => dbContext.Find(id);

    }
}

Continuamos tendo a definição do fluxo segregada de sua execução. Entretanto, dessa vez, estamos usando a interface de consulta do LINQ para definição do fluxo.

Concluindo

Nesse post, mostrei como podemos separar a execução de um fluxo da sua definição. Essa abordagem permite que escrevamos código com execução Lazy. Também vimos como definir fluxo usando LINQ.

Era isso.

Deixe aqui seu comentário... 6 Comentários

  • Olá Elemar!
    Achei essa série de posts interessantíssima, porém uma dúvida surgiu:
    Caso GetEmployeeById fosse um método assíncrono eu não conseguiria resolver (usar await) usando a forma Linq ou Promisse, certo?
    Não consegui imaginar como resolver isso usando essa abordagem :/
    Talvez um BindAsync poderia ser uma saída?
    Como você resolve essas questões?
    Parabéns e Muito Obrigado!

    • E aí Ângelo, nesses casos eu particularmente costumo criar uma versão Sync e outra Async.
      Abraço
      Fabio

      • Isso do método Bind? Eu to pra estudar isso... achei interessante essa abordagem... em alguns casos é um pouco verboso, mas a forma de leitura de uma estrutura como
        var promise =
        AskEmployeeId()
        .Bind(ParseId)
        .Bind(GetEmployeeById);
        // execução.
        var result = promise.Run();
        me parece muito mais interessante do que o convencional!! 🙂

  • Thiago Moreira disse:

    Achei bem complexo o post, o entendimento ainda pra mim é baixo e preciso voltar algumas casas antes de continuar, Você indica algo inicial alguma matéria, blog, bem fácil de se entender a teoria?
    Muito obrigado.

  • E aí Elemar! Parabéns pelo artigo, eu amo essas coisas 🙂

  • Caio Silva disse:

    É possível aplicar este conceito em um sistema já existente? É viável fazer isso?
    Em nossa empresa temos como carro chefe um sistema de gestão de vendas, um sistema grande e que continua a crescer.
    Iniciado a um certo tempo, trabalhamos ainda com WebsForms do Asp.net. É possível e viável começar a aplicar estes conceitos?

Deixe uma Resposta