Skip to main content

8.2 - Functional Programming in C#

Functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing state and mutable data. While C# was originally designed as an object-oriented language, it has evolved to incorporate many functional programming features, allowing developers to leverage functional techniques when appropriate.

🔰 Beginner's Corner: What is Functional Programming?

Think of functional programming like a calculator - you give it inputs, it performs operations, and returns outputs without changing anything else:

┌─────────────────────────────────────────────────────────┐
│ │
│ OBJECT-ORIENTED APPROACH FUNCTIONAL APPROACH │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Object │ │ Function │ │
│ │ │ │ │ │
│ │ ┌──────┐ │ Modifies │ │ Creates │
│ │ │ Data │ │ internal VS │ │ new │
│ │ └──────┘ │ state │ │ values │
│ │ │ │ │ │
│ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────┘

💡 Concept Breakdown: Key Ideas in Functional Programming

  1. Pure Functions - Like mathematical functions, they always return the same output for the same input and don't cause side effects

    // Pure function - always returns the same result for the same inputs
    int Add(int a, int b) => a + b;

    // Impure function - result depends on external state
    int AddToTotal(int value) => total += value; // Modifies external variable
  2. Immutability - Once created, data doesn't change; instead, operations create new data

    // Object-oriented way (mutable)
    person.Age = person.Age + 1; // Modifies existing object

    // Functional way (immutable)
    var olderPerson = person.WithAge(person.Age + 1); // Creates new object
  3. Functions as First-Class Citizens - Functions can be passed around like any other value

// Passing a function as an argument
var adults = people.Where(person => person.Age >= 18);

⚠️ Common Misconceptions

  1. "I can't use loops" - You can use loops, but recursion and higher-order functions are often preferred
  2. "Everything must be immutable" - Practical functional programming often mixes approaches
  3. "It's only for math problems" - Functional techniques work well for many everyday programming tasks

🌟 Why Learn Functional Programming in C#?

  • Cleaner code - Fewer bugs from unexpected state changes
  • Easier concurrency - Immutable data is safer in multi-threaded code
  • Better testability - Pure functions are easier to test
  • LINQ - C#'s LINQ is based on functional concepts and is incredibly powerful

8.2.1 - Immutability

Immutability is a core concept in functional programming where data, once created, cannot be changed. Instead of modifying existing data, operations create new data with the desired changes.

8.2.1.1 - Benefits of Immutability

Immutable data offers several advantages:

  • Thread safety: Immutable objects can be safely shared between threads without synchronization
  • Predictability: Immutable objects always represent the same value
  • Easier reasoning: Code is easier to understand when data doesn't change unexpectedly
  • Simpler debugging: Immutable data helps track the source of bugs

8.2.1.2 - Creating Immutable Types

C# provides several ways to create immutable types:

// Immutable class with readonly fields
public class ImmutablePoint
{
public readonly int X;
public readonly int Y;

public ImmutablePoint(int x, int y)
{
X = x;
Y = y;
}

// Instead of modifying, create a new instance
public ImmutablePoint WithX(int newX) => new ImmutablePoint(newX, Y);
public ImmutablePoint WithY(int newY) => new ImmutablePoint(X, newY);

// Operations return new instances
public ImmutablePoint Add(ImmutablePoint other) =>
new ImmutablePoint(X + other.X, Y + other.Y);
}

// Immutable class with get-only auto-properties (C# 6+)
public class ImmutablePerson
{
public string FirstName { get; }
public string LastName { get; }
public DateTime DateOfBirth { get; }

public ImmutablePerson(string firstName, string lastName, DateTime dateOfBirth)
{
FirstName = firstName;
LastName = lastName;
DateOfBirth = dateOfBirth;
}

// With methods for creating modified copies
public ImmutablePerson WithFirstName(string firstName) =>
new ImmutablePerson(firstName, LastName, DateOfBirth);

public ImmutablePerson WithLastName(string lastName) =>
new ImmutablePerson(FirstName, lastName, DateOfBirth);
}

8.2.1.3 - Records for Immutability (C# 9+)

C# 9 introduced records, which are reference types designed for immutability:

// Immutable record type
public record Person(string FirstName, string LastName, DateTime DateOfBirth);

// Usage
var john = new Person("John", "Doe", new DateTime(1980, 1, 1));

// Non-destructive mutation (creates a new record)
var johnSmith = john with { LastName = "Smith" };

// Equality based on values, not reference
var sameJohn = new Person("John", "Doe", new DateTime(1980, 1, 1));
Console.WriteLine(john == sameJohn); // True

// Deconstruction
var (firstName, lastName, _) = john;

8.2.1.4 - Immutable Collections

The System.Collections.Immutable namespace provides immutable collection types:

using System.Collections.Immutable;

// Creating immutable collections
ImmutableList<int> list = ImmutableList.Create<int>(1, 2, 3, 4);
ImmutableDictionary<string, int> dict = ImmutableDictionary.Create<string, int>()
.Add("one", 1)
.Add("two", 2);
ImmutableHashSet<string> set = ImmutableHashSet.Create<string>("a", "b", "c");

// Non-destructive operations (return new collections)
ImmutableList<int> newList = list.Add(5); // Original list is unchanged
ImmutableDictionary<string, int> newDict = dict.SetItem("one", 10); // Original dict is unchanged

// Builder pattern for batch operations
var builder = list.ToBuilder();
builder.AddRange(new[] { 5, 6, 7 });
builder.Remove(1);
ImmutableList<int> modifiedList = builder.ToImmutable();

8.2.2 - Pure Functions

Pure functions are functions that:

  1. Always produce the same output for the same input
  2. Have no side effects (don't modify state outside the function)

8.2.2.1 - Characteristics of Pure Functions

// Pure function example
public static int Add(int a, int b) => a + b;

// Pure function for string manipulation
public static string Capitalize(string input) =>
string.IsNullOrEmpty(input)
? input
: char.ToUpper(input[0]) + input.Substring(1);

// Pure function with complex logic
public static IEnumerable<int> GetPrimes(int max)
{
if (max < 2)
return Enumerable.Empty<int>();

var isPrime = new bool[max + 1];
for (int i = 2; i <= max; i++)
{
isPrime[i] = true;
}

for (int i = 2; i * i <= max; i++)
{
if (isPrime[i])
{
for (int j = i * i; j <= max; j += i)
{
isPrime[j] = false;
}
}
}

return Enumerable.Range(2, max - 1).Where(i => isPrime[i]);
}

8.2.2.2 - Benefits of Pure Functions

Pure functions offer several advantages:

  • Testability: Easy to test since they depend only on their inputs
  • Memoization: Results can be cached since the same inputs always produce the same outputs
  • Parallelization: Can be executed in parallel without synchronization concerns
  • Reasoning: Easier to understand and debug
// Memoization example
public static class Memoization
{
public static Func<TInput, TOutput> Memoize<TInput, TOutput>(Func<TInput, TOutput> func)
where TInput : notnull
{
var cache = new Dictionary<TInput, TOutput>();

return input =>
{
if (cache.TryGetValue(input, out var cachedResult))
{
return cachedResult;
}

var result = func(input);
cache[input] = result;
return result;
};
}
}

// Usage
Func<int, int> fibonacci = null;
fibonacci = n => n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);

// Memoized version
var memoizedFibonacci = Memoization.Memoize<int, int>(
n => n <= 1 ? n : memoizedFibonacci(n - 1) + memoizedFibonacci(n - 2)
);

Console.WriteLine(memoizedFibonacci(40)); // Fast computation

8.2.2.3 - Avoiding Side Effects

Side effects make functions impure and harder to reason about:

// Impure function with side effects
private static int _counter = 0;

public static int IncrementCounter()
{
return ++_counter; // Side effect: modifies global state
}

// Pure alternative
public static int IncrementValue(int value)
{
return value + 1; // No side effects
}

// Impure function with I/O side effects
public static int ParseAndSave(string input)
{
int value = int.Parse(input);
File.WriteAllText("result.txt", value.ToString()); // Side effect: file I/O
return value;
}

// Pure alternative with side effects moved outside
public static int Parse(string input)
{
return int.Parse(input); // Pure function
}

// Caller handles the side effect
public static void ProcessInput(string input)
{
int value = Parse(input); // Call pure function
File.WriteAllText("result.txt", value.ToString()); // Side effect isolated
}

8.2.3 - Higher-Order Functions

Higher-order functions are functions that take other functions as parameters or return functions as results.

8.2.3.1 - Functions as Parameters

// Higher-order function that takes a function as a parameter
public static List<T> Filter<T>(List<T> items, Func<T, bool> predicate)
{
var result = new List<T>();
foreach (var item in items)
{
if (predicate(item))
{
result.Add(item);
}
}
return result;
}

// Usage
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var evenNumbers = Filter(numbers, n => n % 2 == 0);

// Higher-order function with multiple function parameters
public static List<TResult> Map<TSource, TResult>(
List<TSource> items,
Func<TSource, TResult> mapper)
{
var result = new List<TResult>();
foreach (var item in items)
{
result.Add(mapper(item));
}
return result;
}

// Usage
var squares = Map(numbers, n => n * n);

8.2.3.2 - Functions as Return Values

// Higher-order function that returns a function
public static Func<int, int> CreateMultiplier(int factor)
{
return n => n * factor;
}

// Usage
var double = CreateMultiplier(2);
var triple = CreateMultiplier(3);

Console.WriteLine(double(5)); // 10
Console.WriteLine(triple(5)); // 15

// More complex example: function factory
public static Func<T, bool> CombinePredicates<T>(
Func<T, bool> predicate1,
Func<T, bool> predicate2,
bool useAnd)
{
return useAnd
? item => predicate1(item) && predicate2(item)
: item => predicate1(item) || predicate2(item);
}

// Usage
var isEven = (int n) => n % 2 == 0;
var isPositive = (int n) => n > 0;

var isEvenAndPositive = CombinePredicates(isEven, isPositive, true);
var isEvenOrPositive = CombinePredicates(isEven, isPositive, false);

8.2.3.3 - Closures

Closures are functions that capture variables from their containing scope:

// Function that creates a closure
public static Func<int, int> CreateCounter(int start)
{
int current = start;

return increment =>
{
current += increment;
return current;
};
}

// Usage
var counter1 = CreateCounter(0);
var counter2 = CreateCounter(10);

Console.WriteLine(counter1(1)); // 1
Console.WriteLine(counter1(1)); // 2
Console.WriteLine(counter2(5)); // 15

// Practical example: creating a sequence generator
public static Func<T> CreateSequenceGenerator<T>(IEnumerable<T> sequence)
{
var enumerator = sequence.GetEnumerator();

return () =>
{
if (enumerator.MoveNext())
{
return enumerator.Current;
}
throw new InvalidOperationException("Sequence has ended");
};
}

// Usage
var fibonacci = new List<int> { 0, 1, 1, 2, 3, 5, 8, 13 };
var fibGenerator = CreateSequenceGenerator(fibonacci);

for (int i = 0; i < fibonacci.Count; i++)
{
Console.WriteLine(fibGenerator());
}

8.2.4 - Function Composition

Function composition is the process of combining multiple functions to create a new function.

8.2.4.1 - Basic Function Composition

// Function composition helper
public static Func<T, TResult> Compose<T, TIntermediate, TResult>(
Func<T, TIntermediate> first,
Func<TIntermediate, TResult> second)
{
return input => second(first(input));
}

// Usage
Func<int, int> double = x => x * 2;
Func<int, string> toString = x => x.ToString();

Func<int, string> doubleAndStringify = Compose(double, toString);

Console.WriteLine(doubleAndStringify(5)); // "10"

// Multiple composition
public static Func<T, TResult> Compose<T, T1, T2, TResult>(
Func<T, T1> first,
Func<T1, T2> second,
Func<T2, TResult> third)
{
return input => third(second(first(input)));
}

// Usage
Func<int, int> addOne = x => x + 1;
Func<int, int> square = x => x * x;
Func<int, string> format = x => $"The result is {x}";

var pipeline = Compose(addOne, square, format);
Console.WriteLine(pipeline(5)); // "The result is 36"

8.2.4.2 - Pipeline Operator Simulation

C# doesn't have a built-in pipeline operator, but we can simulate one:

// Extension method to simulate a pipeline operator
public static class PipelineExtensions
{
public static TOutput Pipe<TInput, TOutput>(
this TInput input,
Func<TInput, TOutput> func)
{
return func(input);
}
}

// Usage
var result = 5
.Pipe(x => x + 1) // 6
.Pipe(x => x * x) // 36
.Pipe(x => $"The result is {x}"); // "The result is 36"

// More complex example
public static string ProcessText(string text)
{
return text
.Pipe(s => s.Trim())
.Pipe(s => s.ToLower())
.Pipe(s => s.Replace(" ", "-"))
.Pipe(s => s.Substring(0, Math.Min(s.Length, 50)));
}

// Usage
string processed = ProcessText(" Hello World "); // "hello-world"

8.2.4.3 - Practical Function Composition

// Domain example: Order processing pipeline
public class Order
{
public int Id { get; set; }
public List<OrderItem> Items { get; set; }
public decimal TotalAmount { get; set; }
public bool IsValidated { get; set; }
public bool IsPaid { get; set; }
}

public class OrderItem
{
public int ProductId { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
}

// Processing functions
public static class OrderProcessor
{
public static Order ValidateOrder(Order order)
{
// Validation logic
order.IsValidated = order.Items.Count > 0 && order.TotalAmount > 0;
return order;
}

public static Order CalculateTotal(Order order)
{
order.TotalAmount = order.Items.Sum(item => item.Price * item.Quantity);
return order;
}

public static Order ProcessPayment(Order order)
{
// Payment processing logic
order.IsPaid = true;
return order;
}

// Compose the processing pipeline
public static Func<Order, Order> CreateOrderProcessingPipeline()
{
return order => order
.Pipe(CalculateTotal)
.Pipe(ValidateOrder)
.Pipe(ProcessPayment);
}
}

// Usage
var order = new Order
{
Id = 1,
Items = new List<OrderItem>
{
new OrderItem { ProductId = 101, Quantity = 2, Price = 10.0m },
new OrderItem { ProductId = 102, Quantity = 1, Price = 15.0m }
}
};

var processingPipeline = OrderProcessor.CreateOrderProcessingPipeline();
var processedOrder = processingPipeline(order);

8.2.5 - Recursion in Functional Programming

Recursion is a fundamental technique in functional programming, often used instead of loops.

8.2.5.1 - Basic Recursion

// Factorial using recursion
public static int Factorial(int n)
{
if (n <= 1)
return 1;
return n * Factorial(n - 1);
}

// Fibonacci using recursion
public static int Fibonacci(int n)
{
if (n <= 1)
return n;
return Fibonacci(n - 1) + Fibonacci(n - 2);
}

8.2.5.2 - Tail Recursion

Tail recursion is a special form of recursion where the recursive call is the last operation in the function:

// Tail-recursive factorial
public static int FactorialTailRecursive(int n, int accumulator = 1)
{
if (n <= 1)
return accumulator;
return FactorialTailRecursive(n - 1, n * accumulator);
}

// Tail-recursive Fibonacci
public static int FibonacciTailRecursive(int n, int a = 0, int b = 1)
{
if (n == 0)
return a;
if (n == 1)
return b;
return FibonacciTailRecursive(n - 1, b, a + b);
}

8.2.5.3 - Recursive Data Processing

// Recursive sum of a list
public static int Sum(List<int> list)
{
if (list.Count == 0)
return 0;
return list[0] + Sum(list.Skip(1).ToList());
}

// Recursive map operation
public static List<TResult> Map<TSource, TResult>(
List<TSource> list,
Func<TSource, TResult> mapper)
{
if (list.Count == 0)
return new List<TResult>();

var result = Map(list.Skip(1).ToList(), mapper);
result.Insert(0, mapper(list[0]));
return result;
}

// Recursive tree traversal
public class TreeNode<T>
{
public T Value { get; }
public List<TreeNode<T>> Children { get; }

public TreeNode(T value, List<TreeNode<T>> children = null)
{
Value = value;
Children = children ?? new List<TreeNode<T>>();
}
}

public static IEnumerable<T> TraverseDepthFirst<T>(TreeNode<T> node)
{
yield return node.Value;

foreach (var child in node.Children)
{
foreach (var value in TraverseDepthFirst(child))
{
yield return value;
}
}
}

8.2.6 - Functional Techniques with LINQ

LINQ (Language Integrated Query) is heavily influenced by functional programming and provides many functional operations.

8.2.6.1 - LINQ as a Functional Tool

var numbers = Enumerable.Range(1, 10);

// Functional operations with LINQ
var result = numbers
.Where(n => n % 2 == 0) // Filter
.Select(n => n * n) // Map
.OrderByDescending(n => n) // Sort
.Take(3); // Limit

// Aggregation
int sum = numbers.Sum();
int product = numbers.Aggregate(1, (acc, n) => acc * n);

// Grouping
var grouped = numbers.GroupBy(n => n % 3);

// Flattening nested collections
var nestedLists = new List<List<int>>
{
new List<int> { 1, 2, 3 },
new List<int> { 4, 5, 6 },
new List<int> { 7, 8, 9 }
};

var flattened = nestedLists.SelectMany(list => list);

8.2.6.2 - Functional Composition with LINQ

// Creating reusable query components
IEnumerable<T> Where<T>(IEnumerable<T> source, Func<T, bool> predicate) => source.Where(predicate);
IEnumerable<TResult> Select<T, TResult>(IEnumerable<T> source, Func<T, TResult> selector) => source.Select(selector);
IEnumerable<T> Take<T>(IEnumerable<T> source, int count) => source.Take(count);

// Composing queries
var isEven = (int n) => n % 2 == 0;
var square = (int n) => n * n;

var evenSquares = numbers
.Pipe(source => Where(source, isEven))
.Pipe(source => Select(source, square));

// Query composition with extension methods
public static class QueryExtensions
{
public static IEnumerable<T> WhereEven<T>(this IEnumerable<T> source)
where T : IConvertible
{
return source.Where(item => Convert.ToInt32(item) % 2 == 0);
}

public static IEnumerable<int> Square(this IEnumerable<int> source)
{
return source.Select(n => n * n);
}

public static IEnumerable<T> TakeTopN<T, TKey>(
this IEnumerable<T> source,
int n,
Func<T, TKey> keySelector)
{
return source.OrderByDescending(keySelector).Take(n);
}
}

// Usage
var result = numbers
.WhereEven()
.Square()
.TakeTopN(3, n => n);

8.2.6.3 - Advanced LINQ Patterns

// Monadic operations
public static class OptionExtensions
{
// Option type (Maybe monad)
public class Option<T>
{
private readonly T _value;
private readonly bool _hasValue;

private Option(T value, bool hasValue)
{
_value = value;
_hasValue = hasValue;
}

public static Option<T> Some(T value) => new Option<T>(value, true);
public static Option<T> None() => new Option<T>(default, false);

public bool HasValue => _hasValue;
public T Value => _hasValue ? _value : throw new InvalidOperationException("Option has no value");

// Monadic bind operation
public Option<TResult> Bind<TResult>(Func<T, Option<TResult>> func)
{
return _hasValue ? func(_value) : Option<TResult>.None();
}

// Map operation
public Option<TResult> Map<TResult>(Func<T, TResult> func)
{
return _hasValue ? Option<TResult>.Some(func(_value)) : Option<TResult>.None();
}

// GetValueOrDefault
public T GetValueOrDefault(T defaultValue = default)
{
return _hasValue ? _value : defaultValue;
}
}

// Extension methods for LINQ-like syntax
public static Option<T> FirstOrNone<T>(this IEnumerable<T> source)
{
foreach (var item in source)
{
return Option<T>.Some(item);
}
return Option<T>.None();
}

public static Option<T> FirstOrNone<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
return source.Where(predicate).FirstOrNone();
}

public static Option<TResult> Select<T, TResult>(this Option<T> option, Func<T, TResult> selector)
{
return option.Map(selector);
}

public static Option<TResult> SelectMany<T, TResult>(
this Option<T> option,
Func<T, Option<TResult>> selector)
{
return option.Bind(selector);
}

public static Option<TResult> SelectMany<T, TIntermediate, TResult>(
this Option<T> option,
Func<T, Option<TIntermediate>> intermediateSelector,
Func<T, TIntermediate, TResult> resultSelector)
{
return option.Bind(x =>
intermediateSelector(x).Map(y => resultSelector(x, y)));
}
}

// Usage of monadic LINQ
using static OptionExtensions;

var numbers = new[] { 1, 2, 3, 4, 5 };

// Using Option with LINQ query syntax
var result =
from n in numbers.FirstOrNone(n => n > 3)
let squared = n * n
from m in numbers.FirstOrNone(m => m > squared)
select n + m;

int value = result.GetValueOrDefault(-1);

Summary

Functional programming in C# offers powerful techniques for writing clean, maintainable, and robust code. By embracing immutability, pure functions, higher-order functions, and function composition, you can leverage the strengths of functional programming while still benefiting from C#'s object-oriented features.

LINQ provides a bridge between functional and imperative programming styles, allowing you to express complex data transformations in a declarative, functional manner. As C# continues to evolve, it incorporates more functional programming features, making it an increasingly powerful multi-paradigm language.

Additional Resources