CodeGym /Cursos /C# SELF /Aplicação prática de FP em C#

Aplicação prática de FP em C#

C# SELF
Nível 51 , Lição 4
Disponível

1. Introdução

Indo da teoria para a prática, faz sentido perguntar: “Pra que eu, desenvolvedor .NET e C#, preciso dessas técnicas de programação funcional?”
De fato, C# não é uma linguagem puramente funcional como F# ou Haskell. Mas desde a versão 3.0 até C# 14 ele ganhou várias ferramentas de FP que podem melhorar muito a qualidade e expressividade do código.

Aqui estão os casos onde elas funcionam especialmente bem:

  • Trabalho com coleçõesLINQ, Map/Reduce, filtragem, agregação, ordenação e outras “mágicas” com dados.
  • Funções puras — menos bugs por causa de estado e efeitos colaterais, mais fácil de debugar.
  • Higher-order functions — componentes genéricos e reutilizáveis com os quais é gostoso trabalhar.
  • Imutabilidade em multithreading — um dos principais segredos pra código seguro em cenários paralelos e assíncronos.
  • Composição de funções — deixa lógica de negócio complexa concisa, legível e fácil de testar.

Tabela: Comparação entre abordagem OOP e abordagem FP em C#

Tarefa Imperativo (OOP/velha escola) Funcional (FP)
Filtrar lista
foreach + if + Add
.Where(predicate)
Transformar lista
foreach + cálculo + Add
.Select(lambda)
Buscar por critério
foreach + if/return
.FirstOrDefault(predicate)
Agregação
loop com variável-contadora
.Aggregate(seed, func)
Cache
dicionário manual + checagens
função com closure

2. LINQ: o mais funcional em C#

Se você achou que “programação funcional” é sobre listas, filtros e coisas como .Where, .Select, .Aggregate, — parabéns: é mesmo! LINQ é a quintessência de FP em C#.

Vamos lembrar como o LINQ funciona

LINQ opera sobre coleções com cadeias de métodos que recebem funções como parâmetros (por exemplo, lambdas). Por exemplo:

var numbers = new List<int> { 1, 2, 3, 4, 5 };

// Vamos pegar só os números pares e dobrá-los
var result = numbers
    .Where(x => x % 2 == 0)
    .Select(x => x * 2);

foreach (var number in result)
    Console.WriteLine(number);

O que está acontecendo aqui?

  • .Where — função de ordem superior: recebe uma função (x => x % 2 == 0) e retorna outra coleção.
  • .Select — também recebe uma função (x => x * 2).
  • Não mudamos a coleção original; obtemos um novo resultado.

Esse estilo é fácil de ler e estender (dá pra encadear .OrderBy, .Take, .Distinct etc.).

Nota! Expressões lambda são uma forma prática de criar um delegate na hora. LINQ seria impossível sem suporte a FP em C#.

Esquema: Processamento funcional de uma coleção


Coleção --> Where(x => bool) --> Select(x => y) --> Novo resultado

3. Composição de funções e pipeline de processamento

FP usa muito composição: uma operação complexa é construída como uma cadeia de funções pequenas, cada uma com sua responsabilidade.

Exemplo: pipeline de processamento de string

Estilo com mutação (OOP):

string s = "   hello world   ";
s = s.Trim();
s = s.ToUpper();
s = s + "!";
Console.WriteLine(s); // HELLO WORLD!

Mais “funcional” — como um pipeline de funções:

Func<string, string> trim = x => x.Trim();
Func<string, string> upper = x => x.ToUpper();
Func<string, string> addBang = x => x + "!";

// Composição de funções — aplicamos em sequência
Func<string, string> pipeline = x => addBang(upper(trim(x)));

Console.WriteLine(pipeline("   hello world   ")); // HELLO WORLD!

Combinador simples de composição:

Func<T, R> Compose<T, U, R>(Func<T, U> f, Func<U, R> g) =>
    x => g(f(x));

// Agora o pipeline via Compose:
var pipeline2 = Compose(trim, upper);
pipeline2 = Compose(pipeline2, addBang);

Console.WriteLine(pipeline2("   hello again    ")); // HELLO AGAIN!

4. Trabalhando com imutabilidade: proteção contra bugs

Imutabilidade é um pilar do FP. Não mudamos a estrutura de dados; retornamos uma nova. Isso é crucial em apps multithread.

Exemplo: “errado” (mutável)

List<int> numbers = new List<int> { 1, 2, 3 };
numbers[0] = 42;

Exemplo: “certo” (funcional)

var numbers = new List<int> { 1, 2, 3 };
var newNumbers = numbers.Select((x, i) => i == 0 ? 42 : x).ToList();

No C# moderno existem coleções ImmutableList<T> e outros tipos no namespace System.Collections.Immutable:

using System.Collections.Immutable;

var immutableNumbers = ImmutableList.Create(1, 2, 3);
var changed = immutableNumbers.SetItem(0, 42); // Retorna uma nova lista!

5. Funções de ordem superior na vida real

Higher-order functions permitem escrever componentes genéricos sem montes de ifs.

Exemplo: filtro genérico para usuários

class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

var users = new List<User>
{
    new User { Name = "Vasya", Age = 26 },
    new User { Name = "Katya", Age = 17 },
    new User { Name = "Lyosha", Age = 35 }
};
List<User> FilterUsers(List<User> source, Predicate<User> predicate)
{
    return source.Where(u => predicate(u)).ToList();
}

// Uso:
var adults = FilterUsers(users, u => u.Age >= 18);
var longNames = FilterUsers(users, u => u.Name.Length > 3);

6. Pattern matching e switch-expressions

C# moderno usa bastante pattern matching: switch-expressions frequentemente substituem longas cadeias de ifs.

object value = 123;

string description = value switch
{
    int i when i > 100 => "Número grande",
    string s when s.Length > 3 => "String longa",
    null => "Valor vazio",
    _ => "Desconhecido"
};

Console.WriteLine(description); // Número grande

7. Memoization: cacheando resultados de funções

Memoization é guardar o resultado de uma função para os mesmos argumentos. Em C# dá pra implementar fácil por conta própria.

Func<int, int> SlowFib = null; // Função recursiva de Fibonacci

var cache = new Dictionary<int, int>();

SlowFib = n =>
{
    if (cache.ContainsKey(n))
        return cache[n];
    if (n <= 1)
        cache[n] = n;
    else
        cache[n] = SlowFib(n - 1) + SlowFib(n - 2);
    return cache[n];
};

Console.WriteLine(SlowFib(40)); // Rápido!

8. Currying e aplicação parcial

Aplicação parcial é fixar parte dos argumentos de uma função. Em C# isso é simples com lambdas.

Func<int, int, int> add = (a, b) => a + b;

// Fixando o primeiro argumento
Func<int, int> add10 = b => add(10, b);

Console.WriteLine(add10(5));   // 15
Console.WriteLine(add10(100)); // 110

9. Estilo declarativo com funções

Imperativo:

var result = new List<int>();
foreach (var n in numbers)
{
    if (n > 0)
        result.Add(n * n);
}

Declarativo:

var result = numbers
    .Where(n => n > 0)
    .Select(n => n * n)
    .ToList();

10. Exercício prático

Vamos implementar um módulo de filtragem de tarefas em um “task manager para estudantes”.

Modelo:

class StudentTask
{
    public string Title { get; set; }
    public bool IsCompleted { get; set; }
    public int Priority { get; set; }
}

Dados iniciais:

var tasks = new List<StudentTask>
{
    new StudentTask { Title = "Fazer o dever de casa", IsCompleted = false, Priority = 2 },
    new StudentTask { Title = "Beber um café", IsCompleted = true, Priority = 3 },
    new StudentTask { Title = "Assistir a palestra", IsCompleted = false, Priority = 1 }
};

Filtro genérico:

List<StudentTask> FilterTasks(
    List<StudentTask> all,
    Predicate<StudentTask> predicate)
{
    return all.Where(t => predicate(t)).ToList();
}

// Buscar tarefas não concluídas com prioridade > 1
var importantTasks = FilterTasks(tasks, t => !t.IsCompleted && t.Priority > 1);

// Exibir resultado
foreach (var task in importantTasks)
    Console.WriteLine(task.Title);

Combinadores de predicado:

Predicate<StudentTask> IsActive = t => !t.IsCompleted;
Predicate<StudentTask> IsHighPriority = t => t.Priority > 1;

// Combinando critérios, opção 1
var specialTasks = FilterTasks(tasks, t => IsActive(t) && IsHighPriority(t));

// Opção 2: função-combinador de dois predicados
Predicate<StudentTask> And(Predicate<StudentTask> a, Predicate<StudentTask> b) => t => a(t) && b(t);

var specialTasks2 = FilterTasks(tasks, And(IsActive, IsHighPriority));

11. Particularidades e erros comuns ao aplicar FP em C#

Primeiro, lembre que C# é fortemente tipado. Às vezes é necessário declarar tipos explicitamente, especialmente quando funções retornam delegates ou lambdas complexas. Caso contrário você pode ter erro de compilação por tipo não inferido.

Segundo, não passe funções com efeitos colaterais onde se espera funções puras. Mudar variáveis externas quebra previsibilidade. Procure manter suas funções sem mutar estado fora do seu escopo.

Terceiro, tome cuidado com capture de variáveis do escopo externo (closure capture), especialmente em código assíncrono e multithread. Uma variável cujo valor muda depois de ser capturada pela lambda (por exemplo, em LINQ) pode causar bugs sutis.

Por fim, excesso de estilo FP pode complicar a manutenção para times não acostumados. Use FP quando realmente simplifica a solução, não só por “beleza”.

1
Pesquisa/teste
Programação Funcional, nível 51, lição 4
Indisponível
Programação Funcional
Introdução à programação funcional
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION