Introdução para Programação Funcional com C#

Esse artigo foi traduzido originalmente de: Introduction to functional programming with C# escrito por Naveen Kumar.

Antes de tudo gostaria de convidar a todos que desejam entrar mais a fundo no assunto sobre programação funcional através do curso promovido por uma das pessoas que hoje eu tenho como referencia que é o Jean Carlo Nascimento, porém, todos nós conhecemos carinhosamente como tio Suissa. Enquanto estamos fazendo nossas atividades, Suissa com certeza está estudando funcional ou refatorando para funcional, o cara simplesmente não para!

Promovo aqui o seu curso ao qual deixarei o link abaixo, e que inclusive a tradução desse artigo foi meu exercício juntamente a ele em prol do curso, ao qual estou completamente animado em ter a oportunidade de estudar diretamente com o tio Suissa.

Curso de refatoração para JS Funcional

Esperamos vocês por lá!


Não é nenhuma surpresa que um dos maiores desafios no desenvolvimento de software empresarial é a complexidade. A mudança é inevitável. Especialmente quando um novo recurso é introduzido, e que acrescenta complexidade. Isso nos leva à dificuldades em entender e verificar o software. E não pára por aí, ele aumenta o tempo de desenvolvimento, introduz novos bugs, e em algum momento fica impossível de mudar algo no software sem introduzir algum comportamento inesperado ou efeitos colaterais. Isso pode resultar em retardar o projeto, e eventualmente, levar a uma falha no projeto.

A programação imperativa assim como a programação orientada a objetos têm a capacidade de minimizar a complexidade a um certo nível, quando feito corretamente, criando abstrações e ocultando a complexidade. Um objeto de uma classe tem determinado comportamento que pode ser fundamentado sem se preocupar com a complexidade da implementação. As classes, quando devidamente escritas, terão alta coesão e baixo acoplamento onde a reutilização do código é aumentada enquanto a complexidade é mantida razoável.

A programação orientada a objetos está na minha corrente sanguínea, eu fui programador C# na maior parte da minha vida e meu processo de pensamento sempre tende a usar uma sequência de instruções para determinar como alcançar um determinado objetivo, projetando hierarquias de classes, focando no encapsulamento, abstração e polimorfismo, que tendem a mudar o estado do programa que modifica ativamente a memória. Há sempre uma possibilidade de que qualquer número de threads pode ler um local de memória sem sincronização. Considere que se pelo menos um deles mudar, levaria a uma “race condition”. Não é uma condição ideal para um programador que tenta abraçar a programação simultânea.

No entanto, ainda é possível escrever um programa imperativo escrevendo um código thread-safe completo que suporte simultaneidade. Mas é preciso ainda raciocinar sobre o desempenho, o que é difícil em um ambiente multi-threaded. Mesmo se o paralelismo melhorar o desempenho, é difícil refatorar o código sequencial existente em código paralelo, pois grande parte da base de código precisa ser modificada para usar os threads explicitamente.

Qual é a Solução?

Vale a pena considerar “programação funcional”. É um paradigma de programação originado de ideias mais antigas do que os primeiros computadores quando dois matemáticos introduziram uma teoria chamada cálculo lambda. Ele forneceu uma estrutura teórica que tratou a computação como uma avaliação de funções matemáticas, avaliando expressões ao invés de execuções de comandos e assim, evitando a mudança de estado e mutação de dados.

Ao fazê-lo, é muito mais fácil de entender e raciocinar sobre o código e o mais importante, ele reduz os efeitos colaterais. Além disso, a realização de testes unitários é muito mais fácil.

Linguagens Funcionais:

É interessante analisar as linguagens de programação que suportam programação funcional como Lisp, Clojure, Erlang, OCaml e Haskell que têm sido utilizados em aplicações industriais e comerciais por uma grande variedade de organizações. Cada um deles enfatiza diferentes características e aspectos do estilo funcional. ML é uma linguagem de programação funcional de uso geral, e F# é o membro da família de linguagem ML e originou como uma linguagem de programação funcional para o .Net Framework desde 2002. Combina o estilo sucinto, expressivo e composicional da programação funcional com o tempo de execução, bibliotecas, interoperabilidade e modelo de objeto do .NET

Vale ressaltar que muitas linguagens de programação em geral são flexíveis o suficiente para suportar múltiplos paradigmas. Um bom exemplo é C#, que tem emprestado um monte de recursos de ML e Haskell, embora seja primariamente imperativo com uma forte dependência de mutação de estado. Por exemplo, o LINQ que promove o estilo declarativo e a imutabilidade ao não modificar a coleção subjacente em que atua.

Como já estou familiarizado com C#, eu prefiro combinar paradigmas para que eu tenha controle e flexibilidade ao escolher uma abordagem que melhor se adapta ao problema.

Princípios fundamentais:

Tendo afirmado antes, a programação funcional é a programação com funções matemáticas. A ideia é, sempre que os mesmos argumentos são fornecidos, a função matemática retorna os mesmos resultados e a assinatura da função deve transmitir todas as informações sobre a possível entrada que ele aceita, e a saída ao qual produz. Seguindo dois princípios simples: transparência referencial e honestidade da função.

1. Transparência Referencial:

A transparência referencial, referenciada a uma função, indica que você pode determinar o resultado de aplicar essa função somente observando os valores de seus argumentos. Isso significa que a função deve operar somente nos valores que passamos e não deve se referir ao estado global. Consulte o exemplo abaixo:

public int CalculateElapsedDays(DateTime from)
{
DateTime now = DateTime.Now;
return (now - from).Days;
}

Esta função não é referencialmente transparente. Por quê? Hoje retorna uma saída diferente e amanhã retornará uma outra. A razão é que, refere-se a global DateTime.Now property.

Esta função pode ser convertida em uma função referencialmente transparente? Sim.

Como?

Fazendo uma função para operar somente nos parâmetros ao qual passamos dentro.

public static int CalculateElapsedDays(DateTime from, DateTime now) => (now - from).Days;

Na função acima, eliminamos a dependência do estado global tornando a função transparente.

2. Honestidade da função:

Honestidade da função afirma que uma função matemática deve transmitir todas as informações sobre a possível entrada que ele recebe e a possível saída que produz. Consulte o exemplo abaixo:

public int Divide(int numerator, int denominator)
{
return numerator / denominator;
}

Esta função é referencialmente transparente? Sim.

Qualifica-se como uma função matemática? Talvez não seja.

Motivo: o estado dos argumentos de entrada que leva dois números inteiros a retornar um número inteiro como saída. É verdade em todos os cenários? Não.

O que acontece quando invocamos a função como:

var result = Divide(1,0);

Sim, você adivinhou certo. Ele vai lançar uma exceção “Divide By Zero”. Consequentemente, a assinatura da função não transmite informações suficientes sobre a saída da operação.

Como converter esta função em uma função matemática?

Alterar o tipo de parâmetro denominador, como abaixo

public static int Divide(int numerator, NonZeroInt denominator)
{
return numerator / denominator.Value;
}

NonZeroInt é um tipo personalizado (custom type) que pode conter qualquer número inteiro, exceto zero. Agora, nós fizemos esta função honesta, pois transmite todas as informações sobre a possível entrada que ele pega e a saída que ela produz.

Apesar da simplicidade destes princípios, a programação funcional requer muita prática (muita mesmo), que podem parecer intimidadoras e esmagadoras para muitos programadores. Neste artigo, vou tentar cobrir alguns aspectos básicos para começar com o que inclui “Funções como cidadãos de primeira classe”, “Funções de ordem superior” e “Funções puras”

Funções como cidadãos de primeira classe:

Quando as funções são cidadãos de primeira classe ou valores de primeira classe, eles podem ser usados como entrada ou saída para quaisquer outras funções. Eles podem ser atribuídos a variáveis, armazenados em coleções, assim como os valores de outro tipo. Por exemplo:

Func<int, bool> isMod2 = x => x % 2 == 0;
var list = Enumerable.Range(1, 10);
var evenNumbers = list.Where(isMod2);

O código acima ilustra que as funções são de fato valores de primeira classe porque você pode atribuir a função (x => x% 2 == 0) a uma variável isMod2 que, por sua vez, passou como argumento para o Where (uma extensão no IEnumberable)

O tratamento de funções como valores é necessário no estilo de programação funcional, pois nos dá poder para definir funções de Ordem Superior.

Funções de ordem superior (HOF):

HOF’s são funções que tomam uma ou mais funções como argumentos ou retornam uma função como um resultado ou ambos. Todas as outras funções são funções de primeira ordem.

Vamos considerar o mesmo exemplo no módulo 2 em que “list.Where” faz filtragem para determinar qual número a ser incluído na lista final com base em um predicado fornecido pela chamada. O predicado fornecido aqui é função isMod2 e o método de extensão Wheredo IEnumberable é uma função de ordem superior. Implementação de Wherelogo abaixo:

public static IEnumerable<T> Where<T>
(this IEnumerable<T> ts, Func<T, bool> predicate)
{
foreach (T t in ts) ❶
if (predicate(t)) ❷
yield return t;
}

❶ A tarefa de iterar sobre a lista é um detalhe de implementação de Where.

❷ O critério que determina quais itens são incluídos é decidido pela chamada.

O whereHOF aplica a função dada repetidamente a cada elemento da coleção. HOF’s podem ser projetados para condicionalmente aplicar a função para uma coleção também. Vou deixar isso para você experimentar. Até agora você teria percebido o quão conciso e poderoso são as funções de ordem superior.

Funções Puras:

Funções puras são funções matemáticas que segue os dois princípios básicos que discutimos antes — transparência referencial e honestidade da função. Além disso, as funções puras não causam efeitos colaterais. O que significa que não muda nem o estado global nem os argumentos de entrada. Funções puras são fáceis de testar e raciocinar. Como a saída depende somente da entrada, a ordem de avaliação não é importante. Estas características são muito importantes para um programa ser otimizado para paralelização, Lazy evaluation e Memorização (Caching).

Considere o exemplo de aplicação no exemplo abaixo que multiplica uma lista de números por 2, e que tem uma saída bem formatada para uma tabela de multiplicação.

// Extensions.cs
public static class Extensions
{
public static int MultiplyBy2(this int value)❶
{
return value * 2;
}
}
// MultiplicationFormatter.cs
public static class MultpilicationFormatter
{
static int counter;

static string Counter(int val) => $"{++counter} x 2 = {val}"; ❷

public static List<string> Format(List<int> list)
=> list
.Select(Extensions.MultiplyBy2) ❸
.Select(Counter) ❸
.ToList();
}
// Program.cs
using static System.Console;
static void Main(string[] args)
{
var list = MultpilicationFormatter.Format(Enumerable.Range(1, 10).ToList());
foreach (var item in list)
{
WriteLine(item);
}
ReadLine();
}
// Output
1 x 2 = 2
2 x 2 = 4
3 x 2 = 6
4 x 2 = 8
5 x 2 = 10
6 x 2 = 12
7 x 2 = 14
8 x 2 = 16
9 x 2 = 18
10 x 2 = 20

❶ Uma função pura

❷ Uma função impura como ela, muda o estado global

❸ Funções puras e impuras podem ser aplicadas de forma semelhante

O código acima executa bem sem qualquer problema como nós estamos operando apenas em 10 números. E se quisermos fazer a mesma operação para um conjunto ainda maior de dados, especialmente quando o processamento da CPU é intenso? Teria sentido fazer as multiplicações em paralelo? Isso significa que a sequência de dados pode ser processada de forma independente.

Certamente, podemos usar o poder do paralelismo LINQ (PLINQ) para obter a paralelização para quase livre. Como podemos conseguir isso? Simplesmente adicionando o AsParallel à lista

public static List<string> Format(List<int> list)
=> list.AsParallel()
.Select(Extensions.MultiplyBy2)
.Select(Counter)
.ToList();

Mas espere, funções puras e impuras não paralelizam bem quando estão juntas.

O que quero dizer com isso? Como você sabe que a função Counter é uma função impura. Se você tiver alguma experiência em multi-threading, isso lhe será familiar. A versão paralela terá vários segmentos de leitura e atualização do contador, e não haverá nenhum bloqueio no lugar. O programa vai acabar perdendo algumas atualizações que levarão a resultados de contador incorretos como abaixo:

1 x 2 = 2
2 x 2 = 4
7 x 2 = 6
8 x 2 = 8
6 x 2 = 10
4 x 2 = 12
9 x 2 = 14
10 x 2 = 16
5 x 2 = 18
3 x 2 = 20

Evitando a mutação de estado: Uma maneira possível de corrigir isso é evitar a mutação de estado e executar o contador. Podemos pensar em uma solução para gerar uma lista de contadores que precisamos e mapeá-los a partir da lista dada de itens? Vamos ver como:

using static System.Linq.ParallelEnumerable;
public static class MultpilicationFormatter
{
public static List<string> Format(List<int> list)
=> list.AsParallel()❶
.Select(Extensions.MultiplyBy2)
.Zip(Range(1,list.Count), (val, counter) => $"{counter} x 2 = {val}")❷
.ToList();
}

Usando Zipe Range, reescrevemos o MultiplicationFormatter. Zippode ser usado como um método de extensão, para que você possa escrever o método Formatusando uma sintaxe bem mais fluente. Após essa alteração, o método Formatagora se torna puro. Torna-lo paralelo é apenas uma cereja em cima do bolo ❶. Isso é quase idêntico à versão sequencial, exceto ❶ e ❷

Claro, nem sempre é tão fácil assim como o exemplo deste cenário simples. Mas as ideias que você tem visto até agora irá implantá-lo em uma posição melhor para enfrentar tais questões relacionadas ao paralelismo e a simultaneidade.


No meu próximo artigo, vou tentar desvendar mais alguns aspectos na programação funcional. Fique atento e siga-me se você estiver interessado! Por favor, toque no coração abaixo para recomendar este artigo para que outros possam vê-lo, e claro, façam isso no artigo original.


My sincere thanks to Naveen Kumar for sharing his knowledge through his article!

E meus sinceros agradecimentos ao Suissa pelo apoio prestado ao qual sempre esteve presente desde que nos conhecemos!