C# Funcional
- #C#
Oi pessoal, gostaria de trocar ideias sobre um tópico mais intermediário do C#, que é a possibilidade de programar de maneira funcional, apresentando alguns exemplos aplicados com a API Linq.
Para quem está chegando agora a esse tópico, vou explicar algumas classes ou, melhor dizendo, "tipos funcionais" que o C# oferece e tentar simplificar o assunto.
Primeiramente, é bom termos um entendimento claro do que são funções. De maneira geral, gosto de pensar nelas como pequenos pedaços de código que resolvem um problema muito específico. Além disso, elas podem ter dados de entrada e saída. Vamos ver alguns exemplos:
int Add(int a, int b) => a + b;
void MostraNome(string nome) => Console.WriteLine($"Olá {nome}");
int ElementoNeutroDaSoma() => 0;
bool EhMaiorQueCinco(int numero) => numero > 5;
Vamos analisar as funções acima, devemos pensar em 2 coisas qual o conjunto de entrada e qual o conjunto de saída de cada uma delas, podemos chamar isso de molde da função.
Função Add temos na entrada dois inteiros e na saida um único inteiro.
Molde: (int, int) => int
Função MostraNome temos na entrada uma string e na saida void representado nada.
Molde: (string) => void
Função ElementoNeutroDaSoma não temos parametro de entrada e como saida temos um único inteiro.
Molde: (void) => int
Função EhMaiorQueCinco temos como entrada um inteiro e como saida um bool.
Molde: (int) => bool
Diante dessa análise, a equipe de desenvolvimento do C# decidiu nomear alguns padrões importantes de funções e criar tipos que representassem o modelo que descrevemos acima. São eles:
- Func
- Predicate
- Action
O Func é o tipo mais geral e com ele você pode construir os demais, vamos ver como ele funciona no caso da função Add.
using System;
/*
* Bom primeiro antes de criar a função Add vamos ver o molde dela.
* (int, int) => int
*
* Certo agora vamos ver o molde da interface genérica Func do C#.
* Func<PrimeroParametro, Saida>
* Func<PrimeroParametro, SegundoParametro, Saida>
* Sim temos vários tipos de moldes genéricos para interface Func.
*
* Feito isso podemos simplemente fazer um "de para" do nosso molde de Add para o molde que o C# proporciona.
* (int, int) => int é equivalente a Func<int, int, int>
*/
Func<int, int, int> add = (a, b) => a + b;
// Como usar?
int resultado = add(3,2)
Você deve estar se perguntando: 'Caraca, é possível criar uma variável que representa uma função?' kkkkkk A resposta é sim. Outra pergunta comum é: 'Por que isso é importante?' Bem, eu prometo que até o final desse tópico vou te convencer. Quero que, neste momento, você perceba que com o Func você consegue construir coisas bastante úteis e com pouco código.
Agora que já nos acostumamos com o Func e vimos que ele é capaz de representar qualquer função uma pergunta boa é: "Por que temos Predicate e Action como tipos funcionais?" Por semântica, simplificação e até mesmo impossibilidades de construção, não é possível termos Func<int, void>, mas é totalmente possível criar Action<int>.
O Predicate é equivalente a criação de uma função com quantos parâmetros quisermos, mas o retorno da função está limitado a ser Booleano, vamos ver na construção da nossa função EhMaiorQueCinco.
using System;
/*
* Antes de criar a função EhMaiorQueCinco vamos ver o molde dela.
* (int) => bool
*
* Vamos ver o molde da interface genérica Func do C# que nos interessa.
* Func<PrimeroParametro, bool>
*
* Vamos ver o molde da interface genérica Predicate do C# que nos interessa.
* Predicate<PrimeroParametro>
*
* Note que existe uma equivalencia entre Func<PrimeroParametro, bool> e Predicate<PrimeroParametro> entretando são coisas diferentes para o compilador.
*
* Feito isso podemos simplemente fazer um "de para" do nosso molde de EhMaiorQueCinco para o molde que o C# proporciona.
* (int) => bool é equivalente a Predicate<int>
*/
Predicate<int> ehMaiorQueCinco = (numero) => numero > 5;
Func<int, bool> ehMaiorQueCincoUsandoFunc = (numero) => numero > 5;
// Como usar?
bool eh = ehMaiorQueCinco(2);
bool ehComFunction = ehMaiorQueCincoUsandoFunc(2);
Perceba que o Predicate serve como uma simplificação do Func, mas não são o mesmo tipo para o compilador.
Por fim tenho que explicar o tipo Action, que também serve como uma simplificação ou melhor dizendo um caso especial do Func, vamos construir nossa função MostraNome.
using System;
/*
* Antes de criar a função MostraNome vamos ver o molde dela.
* (string) => void
*
* Vamos ver o molde da interface genérica Action do C# que nos interessa.
* Action<PrimeroParametro>
*
* Feito isso podemos simplemente fazer um "de para" do nosso molde de MostraNome para o molde que o C# proporciona.
* (string) => void é equivalente a Action<string>
*/
Action<string> mostraNome = (nome) => Console.WriteLine($"Olá {nome}");
// Como usar?
mostraNome("Guilherme");
Agora chegou a hora de mostrar a utilidade disso e responder à pergunta 'Por que isso é importante?' Como prometido, eu gostaria realmente de entrar no assunto de 'High Order Functions' neste tópico, mas acredito que seja mais interessante apresentar isso com o Linq em um próximo tópico. Então, vou apenas dar a definição do que seria isso.
'High Order Functions' são funções que recebem uma função como parâmetro e/ou que retornam uma função, vamos ver os dois casos, mas pode ficar tranquilo vou sinalizar pra ti quando isso aparecer.
Feita toda essa apresentação, vamos resolver um problema utilizando programação orientada a objetos pura e programação funcional aliada à orientação a objetos. O problema proposto será muito parecido com um desafio. Quero obter a soma dos n primeiros números ímpares caso eu digite 'i' no console e dos n primeiros números pares caso eu digite 'p'.
Solução com paradigma estruturado e orientado a objetos:
using System;
char tipo = Console.ReadKey();
int n = Convert.ToInt32(Console.ReadLine());
int soma = 0;
for(int i = 0; i <= n; i++){
if(tipo == 'i' && i % 2 != 0) soma += i;
else if(tipo == 'p' && i % 2 == 0) soma += i;
}
Console.WriteLine($"Total: {soma}");
Perceba que não existe muito reuso; os testes de paridade do número são inseridos internamente dentro do laço, assim como o teste do tipo de soma que o usuário pediu via console.
Vamos aplicar tudo o que vimos para tornar esse código mais reutilizável, utilizando Linq, High Order Functions e os tipos funcionais do C#.
Solução com paradigma funcional e orientado a objetos:
using System;
using System.Linq;
int tipo = Console.ReadLine()[0];
int n = Convert.ToInt32(Console.ReadLine());
Func<int, bool> ehPar = (numero) => numero % 2 == 0;
Func<int, bool> ehImpar = (numero) => !ehPar(numero);
/*
* Esta é uma High Order Function, perceba que dado o tipo que queremos calcular
* ela retorna a função correta de teste de paridade.
*
* "Espera ae então da pra fazer injeção de dependência só com High Order Function?"
* "Espera ae então implementar o padrão de projeto Factory só com funções?"
* Sim para as duas kkkkkk, eu sei que pode parecer muito doido, mas isso é lindo kkkkkkkk.
*/
Func<char, Func<int, bool>> passaNoTesteDeParidade = (tipo) => tipo == 'i' ? ehImpar : ehPar;
/*
* Aqui temos outra High Order Function escondida, é o método Where.
* Repare que ele recebe uma função como parâmetro que filtra uma lista gerada pela função Range.
*
* Obs: Enumerable.Range(0, n+1) gera uma lista [0, 1, ..., n]
* E perceba que usei Func<int, bool> ao invés de Predicate<int>, isso foi proposital pois
* o compilador do C# não consegue converter Predicate<int> para Func<int, bool> embora conceitualmente sejam
* iguais.
*/
int soma = Enumerable.Range(0, n + 1).Where(passaNoTesteDeParidade(tipo)).Sum();
Console.WriteLine($"Total: {soma}");
Então é isso, pessoal! Acima, temos um código onde exploramos quase tudo o que vimos aqui nesse tópico e que pode te ajudar bastante quando precisar tornar seu código mais reutilizável. Perceba que agora temos várias funções pequenas que podemos utilizar em vários pontos do nosso código. Além disso, podemos adicionar um novo comportamento de soma de maneira muito simples.
Como desafio, tente adicionar a soma de números divisíveis por 3 no código abaixo. A dica é que basta criar uma função que teste divisão por 3 e modificar nossa função passaNoTesteDeParidade, porém, ela deve continuar funcionando para ímpares e pares.
using System;
using System.Linq;
int tipo = Console.ReadLine()[0];
int n = Convert.ToInt32(Console.ReadLine());
Func<int, bool> ehPar = (numero) => numero % 2 == 0;
Func<int, bool> ehImpar = (numero) => !ehPar(numero);
Func<char, Func<int, bool>> passaNoTesteDeParidade = (tipo) => tipo == 'i' ? ehImpar : ehPar;
int soma = Enumerable.Range(0, n + 1).Where(passaNoTesteDeParidade(tipo)).Sum();
Console.WriteLine($"Total: {soma}");
Comente aí se gostou do tópico e se achou útil o conhecimento passado. Podemos trocar ideias aqui nos comentários sobre a construção deste e de novos tópicos mais complexos de C# :).
Valeu, galera!