1. Introdução
Função pura — é uma função que com os mesmos dados de entrada sempre retorna o mesmo resultado e não causa nenhum efeito colateral. Simplificando: uma função pura não "estraga" nada ao redor e não é afetada por fatores externos.
Uma função é considerada pura se:
- Ela apenas calcula um valor com base nos argumentos de entrada.
- Não modifica variáveis externas ou o estado do programa.
- Não depende de variáveis externas ou estados que possam mudar.
As duas regras de ouro da pureza
- Determinismo: a mesma entrada — a mesma saída.
- Sem efeitos colaterais: a função não muda nada fora de si: nem arquivos, nem variáveis globais, nem a interface do usuário (oi, Console.WriteLine).
Não é religião, é bom senso
Pode parecer uma mania acadêmica, mas a prática mostra o contrário:
- Funções puras são previsíveis. É fácil testá-las — você chama com argumentos específicos e obtém um resultado conhecido.
- Uma função pura pode ser "movida" livremente no código sem medo de quebrar alguma coisa em outros lugares.
- No mundo multicore, função pura é garantia de segurança: dá pra rodar em paralelo sem medo de race conditions.
2. Exemplos de funções puras e impuras
Função pura: tudo previsível
// Função pura: não toca nada fora dela
int Add(int a, int b)
{
return a + b;
}
int Square(int x)
{
return x * x;
}
Chame Add(2, 3) cem vezes — sempre vai retornar 5. Meio chato, mas confiável.
Função impura: quebra as regras
// Quebra a pureza: depende de estado externo (variável estática)
int counter = 0;
int Increase()
{
counter++;
return counter;
}
Aqui chamar Increase() vai devolver um valor diferente cada vez — ou seja, sem determinismo.
// Quebra a pureza: causa efeito externo (imprime na tela)
int AddAndPrint(int a, int b)
{
int sum = a + b;
Console.WriteLine(sum); // Efeito colateral!
return sum;
}
E quanto a aleatoriedade e tempo?
Qualquer função que use DateTime.Now ou Random já não é pura:
// Não é pura!
int GetRandomNumber()
{
return new Random().Next();
}
Tabela de diferenças
| Característica | Função pura | Função impura |
|---|---|---|
| Sempre mesmo resultado para mesmos argumentos | Sim | Não |
| Efeitos colaterais | Não | Sim |
| Dependência de estado externo | Não | Sim |
3. Imutabilidade de dados: teoria e prática
Imutabilidade (immutability) — é a abordagem em que um objeto não pode ser alterado após a criação. Se você precisa de um novo valor — cria um novo objeto.
Por que isso importa?
- O app fica mais resistente a bugs acidentais por alteração de dados.
- Não existem "vazamentos" secretos de mudanças: se você tem um objeto, ninguém vai mudar seus campos às escondidas.
- Imutabilidade é base para muitas otimizações automáticas e para cálculos paralelos.
Exemplos simples em C#
Tipos imutáveis no .NET
Strings (string) em C# são imutáveis! Sempre que você faz string.Concat(s, "world"), uma nova string é criada.
string s = "Hello";
string t = s;
s = s + " World";
Console.WriteLine(t); // t == "Hello"
Arrays e coleções: mutáveis por padrão
int[] numbers = { 1, 2, 3 };
numbers[0] = 42; // O array mudou!
Imutabilidade na prática: compilando código
Ao invés de modificar um objeto/valor existente, retornamos um novo:
// Ao invés disso:
void AddToList(List<int> list, int value)
{
list.Add(value); // Muta!
}
// Melhor assim:
List<int> AddToList(List<int> list, int value)
{
var newList = new List<int>(list) { value }; // Nova lista
return newList;
}
Ilustração: mudar vs não mudar
flowchart LR
A[Objeto original] --"mutação"--> B[Mesmo objeto, mas diferente por dentro]
A --"imutabilidade"--> C[Novo objeto]
4. Por que isso é útil em código C# real?
- No C# moderno, bibliotecas como LINQ, Entity Framework e ASP.NET Core favorecem funções puras e imutabilidade.
- Imutabilidade reduz a quantidade de bugs "mágicos", quando alguém em algum lugar sobrescreve um valor importante.
- Funções puras permitem usar testes automáticos (unit tests) facilmente, porque o teste só precisa checar entrada e saída, sem se preocupar com o mundo externo.
Exemplo: trabalhando com strings
string s = "Hello";
string newS = s.Replace("H", "J"); // s continua "Hello"; newS é "Jello"
Exemplo: LINQ e coleções
Where, Select e outros métodos retornam novas coleções, sem modificar as antigas.
var numbers = new List<int> { 1, 2, 3, 4 };
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
// numbers permaneceu igual!
Exemplo do mundo real: configuração via objetos imutáveis
Muitos APIs modernos do .NET usam objetos imutáveis para configuração, por exemplo JsonSerializerOptions:
var options = new JsonSerializerOptions
{
WriteIndented = true
};
// Esse objeto não é alterado "no meio do caminho", o que aumenta a confiabilidade.
5. Erros e armadilhas típicas
O caminho escorregadio começa quando você "acidentalmente" muta dados em código que deveria ser "puro".
Acontece muito com coleções: você quer filtrar uma lista, mas enquanto faz isso acaba mudando a original.
Ou esquece que um método como List<T>.Add modifica o objeto in-place.
Exemplo traiçoeiro:
List<int> DoubleTheNumbers(List<int> xs)
{
// Erro! Mutamos a lista original, retornamos o mesmo objeto.
foreach (var i in xs)
xs.Add(i * 2);
return xs;
}
Esse código vai até lançar uma exceção em tempo de execução (InvalidOperationException), porque estamos modificando a coleção enquanto a iteramos — problema clássico da mutação.
Correto:
List<int> DoubleTheNumbers(List<int> xs)
{
var newList = new List<int>(xs.Select(x => x * 2));
return newList;
}
GO TO FULL VERSION