Melhorando validações no domínio com melhores assinaturas de método e lógica funcional

Por 29 novembro, 2016CSharp, Programação

Tempo de leitura: 12 minutos

Este post é um pouco denso. Há um bocado de conceitos de programação funcional que podem impor alguma dificuldade a primeira vista. Por isso, para explicar as ideias, usaremos um exemplo de aplicação extremamente simples.

using static System.Console;

class Program
{
    static void Main(string[] args)
    {
        WriteLine("Enter the first number: ");
        var fst = ReadLine();

        WriteLine("Enter the second number: ");
        var snd = ReadLine();

        WriteLine($"First + Second = {Add(fst, snd)}");
    }

    public static double Add(string fst, string snd)
        => double.Parse(fst) + double.Parse(snd);
}

Basicamente, pedimos dois números para o usuário e entregamos uma resposta.

Esse código, embora muito simples, mostra um problema comum nos nossos sistemas: as assinaturas de nossos métodos não dizem exatamente o que ocorrerá.

O que esta assinatura nos diz:

Este método recebe duas strings e devolve um double representando a soma delas.

Não há qualquer menção a uma possível falha.

Melhorando a assinatura do método: indicando a possibilidade de falha na assinatura

Partindo de um post anterior, vamos tornar a possibilidade de falha explícita nessa assinatura.

Mas, antes, vamos adicionar alguns tipos elevados.

Repare que este será um tipo que você irá escrever em sua aplicação uma única vez. Embora não sendo trivial, ele adicionará legibilidade ao seu código

public partial struct Try<TFailure, TSuccess>
{
    internal TFailure Failure { get; }
    internal TSuccess Success { get; }

    public bool IsFailure { get; }
    public bool IsSucess => !IsFailure;

    internal Try(TFailure failure)
    {
        IsFailure = true;
        Failure = failure;
        Success = default(TSuccess);
    }

    internal Try(TSuccess success)
    {
        IsFailure = false;
        Failure = default(TFailure);
        Success = success;
    }

    public static implicit operator Try<TFailure, TSuccess>(TFailure failure)
        => new Try<TFailure, TSuccess>(failure);

    public static implicit operator Try<TFailure, TSuccess>(TSuccess success)
        => new Try<TFailure, TSuccess>(success);

    public TResult Match<TResult>(Func<TFailure, TResult> failure, Func<TSuccess, TResult> success)
        => IsFailure ? failure(Failure) : success(Success);

    public Unit Match(Action<TFailure> failure, Action<TSuccess> success)
        => Match(ToFunc(failure), ToFunc(success));
}

public partial struct Unit
{ }

public static partial class Helpers
{
    private static readonly Unit unit = new Unit();
    public static Unit Unit() => unit;

    public static Func<T, Unit> ToFunc<T>(Action<T> action) => o =>
    {
        action(o);
        return Unit();
    };
}

Voltemos ao exemplo, agora com assinatura aprimorada.

using System;

using static System.Console;
using static Helpers;

class Program
{
    static void Main(string[] args)
    {
        WriteLine("Enter the first number: ");
        var fst = ReadLine();

        WriteLine("Enter the second number: ");
        var snd = ReadLine();

        Add(fst, snd).Match(
            success: result => WriteLine($"First + Second = {result}"),
            failure: WriteLine
            );
    }

    public static Try<Exception, double> Add(string fst, string snd)
    {
        double f, s;

        if (!double.TryParse(fst, out f))
            return new ArgumentException($"Failed to parse '{fst}' to double.", nameof(fst));

        if (!double.TryParse(snd, out s))
            return new ArgumentException($"Failed to parse '{snd}' to double.", nameof(snd));

        return f + s;
    }
}

O que esta assinatura aprimorada nos diz:

Este método recebe duas strings e devolve, possivelmente, um double representando a soma delas. Se houver alguma falha, uma exception será retornada.

Temos algo um pouco mais explícito, mas ainda abaixo do que podemos fazer.

Melhorando a assinatura do método: Deixando mais claro como percebemos os parâmetros

O que pode gerar falhas na execução do nosso código? Sem dúvidas, uma string que não poderia ser convertida em double, certo? Seria correto afirmar que esse seria um parâmetro pouco confiável, como discutimos em um post anterior, certo?

Mais uma vez, perceba que este será um tipo que você irá escrever em sua aplicação uma única vez. Embora não sendo trivial, ele adicionará legibilidade ao seu código

public struct Untrusted<T>
{
    private readonly T _value;

    private Untrusted(T value)
    {
        _value = value;
    }

    public static implicit operator Untrusted<T>(T value)
        => new Untrusted<T>(value);

    public Try<TFailure, TSuccess> Validate<TFailure, TSuccess>(
        Func<T, bool> validation,
        Func<T, TFailure> onFailure,
        Func<T, TSuccess> onSuccess
        ) => validation(_value)
            ? onSuccess(_value)
            : (Try<TFailure, TSuccess>)onFailure(_value);
}

Logo, nosso código poderia ficar assim.

using System;

using static System.Console;
using static Helpers;

class Program
{
    static void Main(string[] args)
    {
        WriteLine("Enter the first number: ");
        var fst = ReadLine();

        WriteLine("Enter the second number: ");
        var snd = ReadLine();

        Add(fst, snd).Match(
            success: result => WriteLine($"First + Second = {result}"),
            failure: WriteLine
            );
    }

    public static Try<FailedToParseDoubleException, double> Add(Untrusted<string> fst, Untrusted<string> snd)
        => ParseToDouble(fst).Match(
            success: f => ParseToDouble(snd)
                .Match<Try<FailedToParseDoubleException, double>>(
                    success: s => f + s,
                    failure: e => e
                ),
            failure: e => e
        );

    private static Try<FailedToParseDoubleException, double> 
        ParseToDouble(Untrusted<string> source)
    {
        var result = 0d;
        return source.Validate(
            s => double.TryParse(s, out result),
            onFailure: s => new FailedToParseDoubleException($"Failed to parse '{s}' to double."),
            onSuccess: _ => result
        );
    }
}

public class FailedToParseDoubleException : Exception
{
    public FailedToParseDoubleException(string message)
        : base(message) { }

    public override string ToString() => Message;
}

Agora a assinatura nos diz:

Este método recebe duas strings pouco confiáveis e devolve, possivelmente, um double representando a soma delas. Se houver alguma falha, uma exception indicando falha de parsing será retornada.

Repare que, até aqui, ainda não há nenhum indicativo na nossa assinatura indicando que as strings serão convertidas em double. Repare também que nosso código não está ficando mais simples.

Melhorando a legibilidade do código: Deixando claro o que estamos esperando

Uma grande falha de nossa assinatura é que ela não indica explicitamente o que estamos esperando. Vamos, mais uma vez, recorrer ao sistema de tipos.

O uso de tipos primitivos indica que estamos negligenciando o domínio. O tipo proposto abaixo é uma especialização bem superior a string.

public struct PotentialDouble
{
    private readonly string _source;
    private readonly double _value;
    private readonly bool _isDouble;

    private PotentialDouble(string source, double value, bool isDouble)
    {
        _source = source;
        _value = value;
        _isDouble = isDouble;
    }

    public static implicit operator PotentialDouble(string source)
    {
        double d;
        return double.TryParse(source, NumberStyles.Any, CultureInfo.InvariantCulture, out d)
            ? new PotentialDouble(source, d, true)
            : new PotentialDouble(source, 0, false);
    }

    public static implicit operator PotentialDouble(double value)
        => new PotentialDouble(null, value, true);

    public TResult Match<TResult>(
        Func<double, TResult> isDouble,
        Func<string, TResult> notIsDouble
        ) => _isDouble
            ? isDouble(_value)
            : notIsDouble(_source);

    public Try<FailedToParseDoubleException, double> ToDouble()
        => Match<Try<FailedToParseDoubleException, double>>(
            isDouble: d => d,
            notIsDouble: source => new FailedToParseDoubleException($"Failed to parse '{source}' to double.")
        );

    public static PotentialDouble Of(string value)
        => value;
}

A vantagem da adoção de um container assim é multiplicar a quantidade de entradas válidas. Por causa da conversão implícita, podemos passar tanto strings quanto doubles agora. Vejamos:

using System;

using static System.Console;
using static Helpers;

class Program
{
    static void Main(string[] args)
    {
        WriteLine("Enter the first number: ");
        var fst = ReadLine();

        WriteLine("Enter the second number: ");
        var snd = ReadLine();

        Add(fst, snd).Match(
            success: result => WriteLine($"First + Second = {result}"),
            failure: WriteLine
            );
    }

    public static Try<FailedToParseDoubleException, double> Add(PotentialDouble fst, PotentialDouble snd)
        => fst.ToDouble().Match(
            success: f => snd.ToDouble().Match<Try<FailedToParseDoubleException, double>>(
                success: s => f + s,
                failure: e => e
                ),
            failure: e => e
        );
}

Agora a assinatura nos diz:

Este método recebe dois doubles em potencial, pouco confiáveis, e devolve, possivelmente, um double representando a soma deles. Se houver alguma falha, uma exception indicando falha de parsing será retornada.

Programação funcional: Usando conceito de aplicação de parâmetros para deixar a lógica mais simples

Quando o código está um pouco complicado, podemos recorrer a ampliação de tipos elevados, como o Try em nosso exemplo, para deixar a lógica um pouco mais explícita.

O tipo Try é extremamente genérico. Embora o código escrito não seja trivial, tem aplicação óbvia.

public partial struct Try<TFailure, TSuccess>
{
    public static Try<TFailure, TSuccess> Of(TSuccess obj) => obj;
    public static Try<TFailure, TSuccess> Of(TFailure obj) => obj;
}

public static class Try
{
    public static Try<TFailure, Func<TB, TResult>> Apply<TFailure, TA, TB, TResult>(
        this Try<TFailure, Func<TA, TB, TResult>> func, Try<TFailure, TA> arg
    )
    {
        return arg.Match(
            failure: e => e,
            success: a => func.Match(
                failure: e2 => e2,
                success: f => Try<TFailure, Func<TB, TResult>>.Of(b => f(a, b))
            )
        );
    }

    public static Try<TFailure, TResult> Apply<TFailure, TA, TResult>(
        this Try<TFailure, Func<TA, TResult>> func, Try<TFailure, TA> arg
    ) => arg.Match(
        failure: e => e,
        success: a => func.Match(
            failure: e2 => e2,
            success: f => Try<TFailure, TResult>.Of((TResult) f(a))
        )
    );
}

O que implementamos aqui é uma lógica simples de aplicação.

Agora nosso código pode ser reescrito de uma forma um pouco mais elegante.

class Program
{
    static void Main(string[] args)
    {
        WriteLine("Enter the first number: ");
        var fst = ReadLine();

        WriteLine("Enter the second number: ");
        var snd = ReadLine();

        Add(fst, snd).Match(
            success: result => WriteLine($"First + Second = {result}"),
            failure: WriteLine
            );
    }

    public static Try<FailedToParseDoubleException, double> Add(PotentialDouble fst, PotentialDouble snd)
    {
        Func<double, double, double> add = (a, b) => a + b;
        return Try<FailedToParseDoubleException, Func<double, double, double>>.Of(add)
            .Apply(fst.ToDouble())   // Try<FailedToParseDoubleException, Func<double, double>>
            .Apply(snd.ToDouble());  // Try<FailedToParseDoubleException, double>

    }
}

Entregando mais: O que fazer quando mais de um erro pode ocorrer

Até aqui estamos assumindo que quando nossa função falhar apenas uma exception será retornada. Mas, isto é correto? Talvez não. Se os dois parâmetros forem inválidos, duas exceptions deveriam ser geradas.

Vamos começar nossa implementação introduzindo mais um tipo elevado.

O tipo Option é outro tipo que adiciona grande poder em nossas implementações. Novamente, embora não seja trivial, não será editado diariamente. Mas, o impacto dele quanto a redução de bugs em tempo de execução pode ser muito grande.

public partial struct Option<T>
{
    internal T Value { get; }
    public bool IsSome { get; }
    public bool IsNone => !IsSome;

    internal Option(T value, bool isSome)
    {
        Value = value;
        IsSome = isSome;
    }

    public TR Match<TR>(Func<T, TR> some, Func<TR> none)
        => IsSome ? some(Value) : none();

    public Unit Match(Action<T> some, Action none)
        => Match(ToFunc(some), ToFunc(none));

    public static readonly Option<T> None = new Option<T>();

    public static implicit operator Option<T>(T value)
        => Some(value);

    public static implicit operator Option<T>(NoneType _)
        => None;
}

public static class Option
{
    public static Option<T> Of<T>(T value)
        => new Option<T>(value, value != null);

    public static Option<TR> Map<T, TR>(
        this Option<T> @this,
        Func<T, TR> mapfunc
        ) =>
            @this.IsSome
                ? Some(mapfunc(@this.Value))
                : None;

    public static T GetOrElse<T>(
        this Option<T> @this,
        Func<T> fallback
        ) =>
            @this.Match(
                some: value => value,
                none: fallback
                );

    public static T GetOrElse<T>(
        this Option<T> @this,
        T @else
        ) =>
            GetOrElse(@this, () => @else);

}

public struct NoneType { }

public static partial class Helpers
{
    public static Option<T> Some<T>(T value) => Option.Of(value);
    public static readonly NoneType None = new NoneType();

    public static Func<Unit> ToFunc(Action action) => () =>
    {
        action();
        return Unit();
    };
}

O tipo Option é bem semelhante ao Try. Ele é uma excelente alternativa para o Null em nossos códigos com a vantagem de não gerar exceptions por usos inadequados. Introduzimos esse tipo elevado para termos um tratamento adequado para requisições de sucesso e falha do tipo Try

public partial struct Try<TFailure, TSuccess>
{
    public Option<TFailure> OptionalFailure
        => IsFailure ? Some(Failure) : None;

    public Option<TSuccess> OptionalSuccess
        => IsSucess ? Some(Success) : None;
}

Com estas ferramentas, conseguimos escrever versões mais potentes das nossas funções de aplicação.

public static partial class Try
{
    public static Try<IEnumerable<TFailure>, Func<TB, TResult>> Apply<TFailure, TA, TB, TResult>(
        this Try<IEnumerable<TFailure>, Func<TA, TB, TResult>> func, Try<TFailure, TA> arg
    )
    {
        return arg.Match(
            failure: e => Try<IEnumerable<TFailure>, Func<TB, TResult>>.Of(
                func.OptionalFailure.GetOrElse(Enumerable.Empty<TFailure>).Concat(new[] { e })
                ),
            success: a => func.Match(
                failure: Try<IEnumerable<TFailure>, Func<TB, TResult>>.Of,
                success: f => Try<IEnumerable<TFailure>, Func<TB, TResult>>.Of(b => f(a, b))
            )
        );
    }

    public static Try<IEnumerable<TFailure>, TResult> Apply<TFailure, TA, TResult>(
        this Try<IEnumerable<TFailure>, Func<TA, TResult>> func, Try<TFailure, TA> arg
    )
    {
        return arg.Match(
            failure: e => Try<IEnumerable<TFailure>, TResult>.Of(
                func.OptionalFailure.GetOrElse(Enumerable.Empty<TFailure>).Concat(new[] { e })
                ),
            success: a => func.Match(
                failure: Try<IEnumerable<TFailure>, TResult>.Of,
                success: f => Try<IEnumerable<TFailure>, TResult>.Of(f(a))
            )
        );
    }
}

Ganhamos também assinaturas mais interessantes:

class Program
{
    static void Main(string[] args)
    {
        WriteLine("Enter the first number: ");
        var fst = ReadLine();

        WriteLine("Enter the second number: ");
        var snd = ReadLine();

        Add(fst, snd).Match(
            success: result => WriteLine($"First + Second = {result}"),
            failure: errors => errors.ToList().ForEach(WriteLine)
            );
    }

    public static Try<IEnumerable<FailedToParseDoubleException>, double> Add(
        PotentialDouble fst, 
        PotentialDouble snd)
    {
        return Try<IEnumerable<FailedToParseDoubleException>, Func<double, double, double>>.Of((a, b) => a + b)
            .Apply(fst.ToDouble())  
            .Apply(snd.ToDouble()); 
    }
}

Agora a assinatura nos diz:

Este método recebe dois doubles em potencial, pouco confiáveis, e devolve, possivelmente, um double representando a soma deles. Se houvere falhas, uma enumeração de exceptions indicando falha de parsing será retornada.

Diminuindo a pressão por código com Monads e Functors

Não pretendo explicar a teoria (pelo menos não agora) para Monads. Mas, as vezes, elas tem utilização clara.

Vamos incrementar um pouco o tipo Try.

public partial struct Try<TFailure, TSuccess>
{
    public Try<TFailure, RR> Map<RR>(Func<TSuccess, RR> func)
        => IsSucess
            ? func(Success)
            : Try<TFailure, RR>.Of(Failure);

    public Try<TFailure, RR> Bind<RR>(Func<TSuccess, Try<TFailure, RR>> func)
        => IsSucess
            ? func(Success)
            : Try<TFailure, RR>.Of(Failure);
}

Por agora, basta sabermos que a função Map classifica instâncias de Try como Functors e a função Bind como Monads.

Esta abordagem nos autoriza escrever nossa função de soma da forma que segue (sem toda a lógica complexa para aplicação que vimos até então)/

public static Try<FailedToParseDoubleException, double> Add(
    PotentialDouble fst, 
    PotentialDouble snd
    ) => fst.ToDouble().Bind(f => snd.ToDouble().Map(s => f + s));

O interessante é que a função Map tem a mesma assinatura da função Select que usamos todos os dias.

public partial struct Try<TFailure, TSuccess>
{
    public Try<TFailure, RR> Select<RR>(Func<TSuccess, RR> func)
        => Map(func);
}

O simples fato de oferecermos essa implementação nos autoriza a utilizar instâncias de Try de forma mais limpa com a sintaxe de consultas LINQ.

var r =
    from number in PotentialDouble.Of("4").ToDouble()
    select number*2; // Try instance with 8

var f =
    from number in PotentialDouble.Of("4").ToDouble()
    select number * 2; // Try instance with Exception

Isso é possível porque, por baixo dos panos, essa sintaxe é convertida pelo compilador para o que segue.

var r =
    PotentialDouble.Of("4").ToDouble().Select(number => number*2); // Try instance with 8

var f =
    PotentialDouble.Of("4").ToDouble().Select(number => number*2); // Try instance with Exception

Como estamos provendo a sobrecarga certa...

Sabemos que quando temos vários usos de from temos, na verdade, o uso da função SelectMany.

public static partial class Try
{
    public static Try<TFailure, RR> SelectMany<TFailure, TSuccess, R, RR>(
        this Try<TFailure, TSuccess> @this,
        Func<TSuccess, Try<TFailure, R>> bind,
        Func<TSuccess, R, RR> projector
    ) => @this.Match(
        failure: Try<TFailure, RR>.Of,
        success: o => @this.Bind(bind).Match(
            failure: Try<TFailure, RR>.Of,
            success: projected => Try<TFailure, RR>.Of(projector(o, projected)))
    );
}

O que nos autoriza a reescrever a função deste nosso cenário novamente de outra forma.

public static Try<FailedToParseDoubleException, double> Add(
    PotentialDouble fst,
    PotentialDouble snd
    ) => 
        from f in fst.ToDouble()
        from s in snd.ToDouble()
        select f + s;

Poderíamos continuar ...

Concluindo

Nesse post discuti a importância de criarmos assinaturas mais expressivas para nossos códigos. Também apresentei maneiras de deixar o código mais fluído através da aplicação de conceitos de programação funcional.

Como mencionei diversas vezes durante o post, tipos como Try, Untrusted e Option não são escritos todos os dias. Mas, representam importantes conceitos que podem deixar nossos códigos mais limpos.

Era isso.

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

  • Joao disse:

    Puta perda de tempo...
    Não necessita tudo isso, isso é preciosismo, está complicando uma validação simples.

    Para uso acadêmico é ótimo, no dia a dia, não é usual.

    • elemarjr disse:

      João,

      No primeiro parágrafo desse post eu destaquei que o exemplo era intencionalmente simples para poder focar nas técnicas. Entendo que o post seja longo e aceito que você possa ter ignorado essas primeiras linhas.

      Quanto a aplicabilidade, tenho usado essa abordagem há tempos. O lado bom é que meus códigos não tem acusado falhas. Se você consegue entregar código com poucas falhas sem tanto preciosismo isso mostra que você é um programador melhor que eu. Ficaria feliz em ver um pouco de seus exemplos. Meus códigos rodam em milhares de pontos, por isso sou tão "preciosista". As vezes, não é mesmo necessário.

      Abraços,

  • Achei excessivo, Elemar. E dá pra usar outras técnicas pra garantir que o código não vai falhar. O código não me parece mais expressivo, mas menos. C# não tem checked exceptions e tentar criar uma estrutura parecida não me pareceu bom. Talvez eu precise ver mais esse código em produção para ver valor, mas nesse momento estou vendo o que parece ser muita complexidade acidental.
    C# também não é F#. Talvez, se a busca for essa, a linguagem esteja errada.
    Abraço!

    • elemarjr disse:

      Você está me dizendo que:

      public double Add(string first, string second) { /*..*/ }

      é menos expressivo que:

      public Try<Exception, double> Add(PotentialDouble first, PotentialDouble second) { /*..*/ }

      Sério?!

      Em tempo, não é código experimental, é código de produção. Que técnica você adotaria aqui para não falhar?

      • Assumindo que esse então não é só um exemplo, que é código de um domínio real, nada mais fácil do que fazer:

        if (!double.TryParse(string first, out d)) WriteLine("Não rolou");

        É pra isso que esse método existe.

        • elemarjr disse:

          Sim, Giggio. O problema é que essa linha é algo interno para implementação. Não há indicativos na assinatura do método de que esse será o procedimento que deverá ser executado. Também não há garantias de que o programador fará implementação cuidadosa e, pior de tudo, não há uma indicação clara de como ele deve proceder se essa verificação falhar..

  • Rodrigo Venturin disse:

    Pura masturbação funcional, para estudo é interessante, mas colocar isto em prática num desenvolvimento em equipe é desaconselhável na grande maioria dos cenários.
    Existem N maneiras de evitar o problema de lançar a exception se for o caso, só que mais simples.
    Entendo seu ponto Elemar, está se deleitando na programação funcional e quer trazer um pouco disso para o C#, fazia tempo que não lia um artigo seu, mas deu para notar sua tendência.

  • Ricardo disse:

    Eu imagino esse código (e tantos outros) sendo usado num aplicativo realmente grande de desenvolvimento ágil e distribuido onde o código (e não a documentação ou as convenções) precisa ser auto-suficiente, tanto em termos de funcionalidade quanto em termos dos pré-requisitos de cada dado que é processado. Além de, em caso de falha, trazer essa falha o mais rápido possível, ajuda a alertar o desenvolvedor que utiliza a biblioteca de funções. É um código extremamente divertido - parabéns por compartilhar com todos as suas técnicas para desenvolver com intenção.

  • Bacana, Elemar. Sempre aprendo com seus posts.
    Depois pretendo implementar para chegar a uma opinião melhor.
    Ao ler o artigo, acabei me lembrando do Maybe , que segue a ideia de deixar mais claro a intenção ao dizer que o valor "talvez" seja retornado, ao invés de retornar null. O que acha? Já chegou a utilizar?
    []s

  • Sim! O conceito de Monads não é algo trivial, mas traz expressividade e mais segurança sobre os Inputs e Outputs de uma função. Além disso, Trazer implementações de conceitos funcionais, já é uma realidade no C# há alguns anos, quando o LINQ, através das Lambdas Expressions. Além desses conceitos abordados aqui no Post, o que muitos implementam diariamente e não se dão por conta é o conceito de: Primitive Obsession. Implementar algo dessa natureza, mesmo que ainda seja complexo no primeiro momento da implementação, traz uma segurança - conforme já falei, para que o código seja mais expressivo e sincero sobre o que ele está tendo de entrada e saída. Não vejo nenhum problema em trazer conceitos funcionais para dentro do C#.

    Parabéns, Elemar! Pelo compartilhamento do conhecimento e riqueza de informações no seu Post.

  • Yan de Lima Justino disse:

    Achei o conceito bem interessante! Acredito que em um cenário de operações críticas valha a aplicabilidade da técnica. Parabéns, Elemar!

  • Vinicius disse:

    Elemar, apaga esse post vai.
    Abraço

  • Fabio disse:

    Oi, Elemar!
    Achei o post interessante, mas confesso que achei um pouco difícil de entender. Gostaria de saber até quando isso seria mais vantajoso do que um simples double.TryParse(..).

  • Henrique Costa disse:

    Antes tudo, temos de analisar um detalhe. Aqui é um blog técnico, escrito por um técnico de nível avançado. A utilização destes conceitos ou não vai de cada um. O ponto é que o Elemar deixou mais uma forma de como efetuar algo, complexo , nada trivial mas inegavelmente interessante.

Deixe uma Resposta