Jump to content

Functional Programming

From Knowledge Base

Functional Programming

Functional programming in C# can complement both imperative and object-oriented programming paradigms. Let's get started with functional programming in C#:

Delegates

C# provides delegates and events, which are critical for functional programming - these include Action and Func.

Action Delegate in C#:

The Action delegate is a predefined delegate type in C# that represents a method which performs an action and does not return a value. It can take parameters, but it does not return any value (it returns void).

Action<string> greet = name => Console.WriteLine($"Hello, {name}!");
greet("World"); // Output: Hello, World!

Func Delegate in C#

The Func delegate type represents a method that takes zero or more input parameters and returns a value. The last type parameter specifies the return type.

Func<int, int, int> sum = (x, y) => x + y;
int result = sum(2, 3); // result = 5
Console.WriteLine(result); // Output: 5

Basics

Lambda Expressions

C# provides lambda expressions and closures, which are essential for functional programming. These allow you to define and pass functions as arguments to other functions.

 Func<int, int> square = x => x * x;
 int result = square(5); // result will be 25
 

Immutable Data

Immutable data structures ensure that once a data structure is created, it cannot be modified.

public class ImmutablePerson {
    public readonly string Name;
    public readonly int Age;
    
    public ImmutablePerson(string name, int age) {
        Name = name;
        Age = age;
    }
}
    

Immutability helps to avoid side effects and maintain a clear flow of data through functions.

LINQ

LINQ in C# that enables functional-style querying of collections.

Higher-Order Functions

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

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
    
List<int> squareNumbers = numbers.Select(x => x * x).ToList(); // squareNumbers contains [1, 4, 9, 16, 25]

Pure Functions

Pure functions have no side effects and always return the same output for the same input.

int Add(int a, int b) {
    return a + b;
}

Functional Composition

Functional composition involves combining smaller functions to create more complex ones.

Func<int, int> doubleNumber = x => x * 2;
Func<int, int> squareNumber = x => x * x;

Func<int, int> compose = x => doubleNumber(squareNumber(x));

int result = compose(3); // Result: 18 (first square, then double)

Pattern Matching

Pattern matching in C# allows you to write more expressive and functional code when dealing with complex data structures. It's a valuable feature in functional programming.

public record Circle(double Radius);
public record Rectangle(double Width, double Height);
public record Triangle(double Base, double Height);

static double CalculateArea(object shape) => shape switch {
    Circle circle => Math.PI * Math.Pow(circle.Radius, 2),
    Rectangle rectangle => rectangle.Width * rectangle.Height,
    Triangle triangle => 0.5 * triangle.Base * triangle.Height,
    _ => throw new ArgumentException("Unknown shape")
};

static void Main() {
    Circle circle = new Circle(5.0);
    Rectangle rectangle = new Rectangle(4.0, 6.0);
    Triangle triangle = new Triangle(3.0, 4.0);

    Console.WriteLine($"Circle area: {CalculateArea(circle)}");  // Circle area: 78.53981633974483
    Console.WriteLine($"Rectangle area: {CalculateArea(rectangle)}");  // Rectangle area: 24
    Console.WriteLine($"Triangle area: {CalculateArea(triangle)}");  // Triangle area: 6
}

Monads

Monads encapsulates and abstracts values to enable consistent and controlled manipulation, composition and transformation of those value.

int? number = 42;
int? result = number
    .Select(x => x * 2)
    .Where(x => x > 50)
    .FirstOrDefault();

Console.WriteLine(result); // Result: 84

Option Types (Optional Values)

An option type represents values that can be either present or absent (null), providing a safer and more expressive way to handle potentially missing data. Option Types (Handling Optional Values):

int? someValue = 42; // A value exists.
int result = someValue ?? 0; // If someValue is null, use a default value (0).

There are libraries and frameworks available in C# that can assist you in functional programming, such as LanguageExt and F#.

Concepts

Recursion

Functional programming encourages the use of recursion for solving problems. Recursion is the process of a function calling itself to solve a problem. Make sure to learn about tail recursion and how to optimize recursive functions in C#. Using recursion to calculate the factorial of a number in C#, without additional functions or set-up:

using System;

static class RecursionExample {
    static int Factorial(int n) => n == 0 ? 1 : n * Factorial(n - 1);

    static void Main() {
        int n = 5;
        Console.WriteLine($"Factorial({n}) = {Factorial(n)}");
    }
}

Currying

Currying is the process of converting a function that takes multiple arguments into a series of functions, each taking a single argument. In C#, you can implement currying using lambda expressions:

Func<int, Func<int, int>> curriedAdd = a => b => a + b;
int addTwo = curriedAdd(2);
int result = addTwo(3); // result will be 5

curriedAdd is a curried function that takes two integers and returns a new function that adds them. This concept can help with partial function application and creating more reusable functions.

Partial Function Application

Partial function application is a technique where you fix a certain number of arguments of a function, creating a new function with fewer parameters. In C#, you can achieve this using lambda expressions as well:

Func<int, int, int> add = (a, b) => a + b;
Func<int, int> addTwo = a => add(a, 2);
int result = addTwo(3); // result will be 5

Here, addTwo partially applies the add function by fixing one of its arguments to 2, resulting in a new function that only requires one input.

Memoization

Memoization is a caching technique where the results of expensive function calls are stored and reused when the same inputs occur again. You can implement memoization in C# to optimize recursive or computationally expensive functions, making them more efficient. This demonstrates memoization for Fibonacci numbers using a Dictionary for caching:

using System;
using System.Collections.Generic;

static class MemoizationExample {
    static Dictionary<int, long> memo = new Dictionary<int, long>();

    static long Fibonacci(int n) =>
        n <= 1 ? n : (memo.ContainsKey(n) ? memo[n] : (memo[n] = Fibonacci(n - 1) + Fibonacci(n - 2)));

    static void Main() {
        Console.WriteLine($"Fibonacci(40) = {Fibonacci(40)}");
    }
}

Monads

Monads are a fundamental concept in functional programming that provide a way to handle sequences of operations, error handling and side effects in a clean and composable manner. In C#, you can work with monads for tasks like asynchronous programming using Task<T>, error handling using Option or Either types, or working with sequences using LINQ. This Task monad for asynchronous programming uses await with Task.Delay to simulate asynchronous operations and processing.

using System;
using System.Threading.Tasks;

static class MonadExample {
    static async Task Main() {
        int data = await ReadDataAsync();
        int result = await ProcessDataAsync(data);
        Console.WriteLine($"Result: {result}");
    }

    static async Task<int> ReadDataAsync() => await Task.Delay(1000).ContinueWith(_ => 42);
    static async Task<int> ProcessDataAsync(int data) => await Task.Delay(500).ContinueWith(_ => data * 2);
}