CodeGym /Courses /C# SELF /Practical Use of FP in C#

Practical Use of FP in C#

C# SELF
Level 51 , Lesson 4
Available

1. Introduction

Moving from theory to practice, it makes sense to ask: "Why should I, a .NET/C# developer, care about all these functional programming techniques?"
Indeed, C# isn't a purely functional language like F# or Haskell. But since version 3.0 up to C# 14, it got lots of FP tools that can seriously improve code quality and expressiveness.

Here are the areas where they shine:

  • Working with collectionsLINQ, Map/Reduce, filtering, aggregation, sorting and other data "magic".
  • Pure functions — fewer bugs from state and side effects, easier to debug.
  • Higher-order functions — generic, reusable components that are pleasant to work with.
  • Immutability in multithreading — one of the main guarantees of safe code in parallel and async scenarios.
  • Function composition — makes complex business logic concise, readable and testable.

Table: Comparing OOP approach and FP approach in C#

Task Imperative (OOP/old school) Functional (FP)
Filtering a list
foreach + if + Add
.Where(predicate)
Transforming a list
foreach + computations + Add
.Select(lambda)
Searching by criterion
foreach + if/return
.FirstOrDefault(predicate)
Aggregation
loop with accumulator variable
.Aggregate(seed, func)
Caching
manual dictionary + checks
function with closure

2. LINQ: the most functional thing in C#

If you thought "functional programming" is about lists, filters and those .Where, .Select, .Aggregate, — congratulations: that's true! LINQ is the quintessence of FP in C#.

Let's recall how LINQ works

LINQ operates on collections using chains of methods that accept function parameters (e.g. lambdas). For example:

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

// Get only even numbers and double them
var result = numbers
    .Where(x => x % 2 == 0)
    .Select(x => x * 2);

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

What's happening here?

  • .Where — a higher-order function: it takes a function (x => x % 2 == 0) and returns another collection.
  • .Select — also takes a function (x => x * 2).
  • We don't mutate the original collection; we get a new result.

This style is easy to read and extend (you can attach .OrderBy, .Take, .Distinct, etc.).

Note! Lambda expressions are a convenient way to create a delegate on the fly. LINQ wouldn't be possible without FP support in C#.

Diagram: Functional processing of a collection


Collection --> Where(x => bool) --> Select(x => y) --> New result

3. Function composition and data processing pipeline

FP often uses composition: a complex operation is built as a chain of small functions, each doing its own job.

Example: string processing pipeline

State-changing style (OOP):

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

More "functional" — as a pipeline of functions:

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

// Function composition — apply them in sequence
Func<string, string> pipeline = x => addBang(upper(trim(x)));

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

Simple compose combinator:

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

// And now pipeline via Compose:
var pipeline2 = Compose(trim, upper);
pipeline2 = Compose(pipeline2, addBang);

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

4. Working with immutability: bug protection

Immutability is a core brick of FP. We don't mutate data structures; we return new ones. This is especially important in multithreaded apps.

Example: "wrong" (mutable)

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

Example: "right" (functional)

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

In modern C# there are collections like ImmutableList<T> and other types in the namespace System.Collections.Immutable:

using System.Collections.Immutable;

var immutableNumbers = ImmutableList.Create(1, 2, 3);
var changed = immutableNumbers.SetItem(0, 42); // Returns a new list!

5. Higher-order functions in real life

Higher-order functions are a way to write generic components without lots of conditional logic.

Example: Generic user filter

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();
}

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

6. Pattern matching and switch expressions

Modern C# actively uses pattern matching: switch expressions often replace heavy chains of if.

object value = 123;

string description = value switch
{
    int i when i > 100 => "Big number",
    string s when s.Length > 3 => "Long string",
    null => "Empty value",
    _ => "Unknown"
};

Console.WriteLine(description); // Big number

7. Memoization: caching function results

Memoization is caching a function's result for identical arguments. In C# you can implement it yourself easily.

Func<int, int> SlowFib = null; // Recursive 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)); // Blazing fast!

8. Currying and partial application

Partial application is fixing part of a function's arguments. In C# it's convenient to do with lambdas.

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

// Fix the first argument
Func<int, int> add10 = b => add(10, b);

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

9. Declarative style with functions

Imperative:

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

Declarative:

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

10. Practical task

Let's implement a task filtering module in a "student task manager".

Model:

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

Initial data:

var tasks = new List<StudentTask>
{
    new StudentTask { Title = "Do homework", IsCompleted = false, Priority = 2 },
    new StudentTask { Title = "Drink coffee", IsCompleted = true, Priority = 3 },
    new StudentTask { Title = "Watch lecture", IsCompleted = false, Priority = 1 }
};

Generic filter:

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

// Find incomplete tasks with priority > 1
var importantTasks = FilterTasks(tasks, t => !t.IsCompleted && t.Priority > 1);

// Print result
foreach (var task in importantTasks)
    Console.WriteLine(task.Title);

Predicate combinators:

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

// Combine multiple criteria, option 1
var specialTasks = FilterTasks(tasks, t => IsActive(t) && IsHighPriority(t));

// Option 2: combinator function for two predicates
Predicate<StudentTask> And(Predicate<StudentTask> a, Predicate<StudentTask> b) => t => a(t) && b(t);

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

11. Peculiarities and common mistakes when applying FP in C#

First, remember that C# is strongly typed. Sometimes you need to specify types explicitly, especially when functions return delegates or complex lambdas. Otherwise you might get a compiler error due to undeduced types.

Second, don't pass functions with side effects where pure functions are expected. Mutating external variables breaks predictability. Try to keep your functions from mutating state outside their scope.

Third, be careful with closure capture, especially in async and multithreaded code. A variable whose value changes after being captured by a lambda (for example, in LINQ) can lead to non-obvious bugs.

Finally, excessive FP style can make code harder to maintain for a team not used to it. Use FP where it genuinely simplifies the solution, not just for "beauty".

2
Task
C# SELF, level 51, lesson 4
Locked
String Processing with Function Composition
String Processing with Function Composition
1
Survey/quiz
Functional Programming, level 51, lesson 4
Unavailable
Functional Programming
Intro to Functional Programming
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION