Functional Programming Basics Every C# Developer Can Use Today

Code today, read with confidence tomorrow.If you’ve ever opened a six-month-old method and thought “what was I even trying to do here?” — then this article is definitely for you. I’ve always thought “how can I make my code cleaner and less complex&quot…


This content originally appeared on Level Up Coding - Medium and was authored by Cristian Toader

Code today, read with confidence tomorrow.

If you’ve ever opened a six-month-old method and thought “what was I even trying to do here?” — then this article is definitely for you. I’ve always thought how can I make my code cleaner and less complex", and once I’ve discovered functional languages like Scala I knew right away.

You don’t need to switch to Scala or F# to benefit from these ideas though — functional programming concepts apply directly in C#. Will this be complicated? Not at all, just a bit unfamiliar at first. Stick around, and I’ll show you how they can make your C# code easier to reason about.

I’m more of an engineer than a writer, so let’s jump right into hands-on coding examples that will show you these principles in action.

Higher order functions

Let’s play with some sums

You’re given a short assignment (let’s consider it a product requirement) to be able to compute the total sum of an array of numbers. You’ve probably written this for loop thousands of times so let’s do it once more.

static int ComputeSum(int[] numbers)
{
var sum = 0;

for (var i = 0; i < numbers.Length; i++)
{
sum += numbers[i];
}

return sum;
}

Great — looking good. New product requirements now: we want to also be able to compute sums for all odd numbers. Let’s get to it. So far our code looks like this.

public static int ComputeSum(int[] numbers)
{
var sum = 0;

for (var i = 0; i < numbers.Length; i++)
{
sum += numbers[i];
}

return sum;
}

public static int ComputeOddsSum(int[] numbers)
{
var sum = 0;

for (var i = 0; i < numbers.Length; i++)
{
if (numbers[i] % 2 == 1)
{
sum += numbers[i];
}

}

return sum;
}

New product requirements just in — we want to compute sums for even numbers too. At this point you might be starting to feel a bit bad about yourself, and feel that you’re doing something wrong in writing yet another for loop, and you’re right!

Let’s have a look at the code below, and introduce our first functional programming concept — higher order functions.

static void Main(string[] args)
{
var numbers = new int[] { 1, 2, 3, 4, 5, 6 };

var sumAll = ConditionalSum(numbers, it => true);
Console.WriteLine($"Sum of all numbers: {sumAll}");

var evensSum = ConditionalSum(numbers, it => it % 2 == 0);
Console.WriteLine($"Sum of even numbers: {evensSum}");

var evensOdds = ConditionalSum(numbers, it => it % 2 == 1);
Console.WriteLine($"Sum of odd numbers: {evensOdds}");
}

public static int ConditionalSum(int[] numbers, Predicate<int> condition)
{
var sum = 0;

for (var i = 0; i < numbers.Length; i++)
{
if (condition.Invoke(numbers[i]))
{
sum += numbers[i];
}
}

return sum;
}

“Higher order functions” are just a name for a simple concept, they are functions that accept other functions as input or return functions. In functional programming, functions can be passed around like any other object, and that’s something very powerful that you can play with if you think about it.

OK — now let’s complicate our example further to stress what’s possible. While maintaining all current functionality, we want to be able to sum squared numbers either odds or evens or all (we do not want to lose any existing functionality).

static void Main(string[] args)
{
var numbers = new int[] { 1, 2, 3, 4, 5, 6 };

var sumSquaredOddNumbers = ConditionalSum(
numbers,
it => it % 2 == 1,
it => it * it
);

Console.WriteLine($"Sum of all numbers: {sumSquaredOddNumbers}");
}

public static int ConditionalSum(int[] numbers,
Predicate<int> condition,
Func<int, int> mapper)
{
var sum = 0;

for (var i = 0; i < numbers.Length; i++)
{
if (condition.Invoke(numbers[i]))
{
sum += mapper.Invoke(numbers[i]);
}
}

return sum;
}

Although not the main focus of this article, all this could be simplified even further using LINQ, which is the C# way of handling collection-based operations in a declarative (and functional) way. Let’s compare LINQ with how this code would have looked like in an imperative style.

public static int ConditionalSumOddNumbersFunctional(int[] numbers)
{
return numbers.Where(it => it % 2 == 1)
.Select(it => it * it)
.Sum();
}

public static int ConditionalSumOddNumbersImperative(int[] numbers)
{
var sum = 0;

for (var i = 0; i < numbers.Length; i++)
{
if (numbers[i] % 2 == 1)
{
var squared = numbers[i] * numbers[i];
sum += squared;
}
}

return sum;
}

So which one is simpler (although maybe not as familiar)?

In the ConditionalSumOddNumbersFunctional method, we just declare what we want: we want odd numbers, we want them squared, and we want the sum.

In the imperative variant we want the same things, but we have to micromanage everything that happens: we need to maintain index i and not go out of bounds, we need to maintain a sum, and nest logic in conditions.

Pure functions

Let’s dig into some brief theory based on the previous examples

The core building block in functional programming is the concept of pure functions. We reference them in variables, we compose them with other functions. We can express any imperative application using pure functions and functional programming. But what exactly makes a function pure?

Pure functions are like mathematical functions, they only take input and produce output and nothing else. They hold a property called “referential transparency” which means that if we were to replace the function call with the result of the functional call everywhere within our code, the execution would be identical.

There are two implications on referential transparency, they are subtle but very strong.

First of all, a pure function doesn’t change anything behind the scenes (e.g. state) — or in functional terms, there are no “side effects”.

Another implication is that pure functions cannot create mutations. They don’t change variables, they only return a value. They promote “immutable” code, which is a key simplification when working with multi-threaded applications.

OK — let’s get back to coding now to see why this is useful and how it drives cleaner code.

Shopping Cart — Imperative vs Functional

Let’s build a shopping cart

New product requirements arrive, this time we switch focus to a new project where we need to create a shopping cart with some basic functionality.

The product manager wants us to support adding items in the cart, apply discounts, and perform a checkout to determine what is the total the customer needs to pay.

Let’s have a look at the imperative implementation of the requirements below.

public class ShoppingCart
{
private readonly List<Product> _items = new();
private decimal _total = 0m;
private decimal _discount = 0m;

public void AddProduct(Product product)
{
_items.Add(product);
_total += product.Price;
}

public void ApplyDiscount(decimal percentage)
{
if (percentage < 0 || percentage > 1)
throw new ArgumentException("Invalid discount");

_discount = percentage;
_total = _total - (_total * percentage);
}

public decimal Checkout()
{
if (_discount > 0)
{
_total = _total - (_total * _discount);
}

return _total;
}

public IReadOnlyList<Product> Items => _items.AsReadOnly();
}

public record Product(string Name, decimal Price);

Before refactoring, consider why the imperative version is problematic: its internal state changes, and its behavior depends on method call-order which is not inherently safe — these are common sources of bugs and make the code harder to reason about and test.

There’s also a bug in the code above, but I’ll let you figure it out on your own just to stress my point.

Now — is there an objectively better way which is easier to understand while maintaining the same functionality? Let’s have a look at the functional implementation below.

public record Product(string Name, decimal Price);

public record ShoppingCart(IReadOnlyList<Product> Items, decimal Discount)
{
public static ShoppingCart Empty => new ShoppingCart(Array.Empty<Product>(), 0m);

public ShoppingCart AddProduct(Product product) =>
this with
{
Items = Items.Append(product).ToList()
};

public Result<ShoppingCart> ApplyDiscount(decimal percentage)
{
if (percentage < 0m || percentage > 1m)
return Result<ShoppingCart>
.Failure("Discount must be between 0.0 and 1.0 (inclusive).");

var newCart = this with { Discount = percentage };
return Result<ShoppingCart>.Success(newCart);
}

/// Pure calculation of total (no side-effects)
public decimal Checkout() =>
Items.Sum(p => p.Price) * (1 - Discount);
}

We’ve also added the Result class as a return type for applying discounts that may either succeed or lead to a failure.

public record Result<T>
{
public bool IsSuccess { get; init; }
public T? Value { get; init; }
public string? Error { get; init; }

public static Result<T> Success(T value) => new() {
IsSuccess = true,
Value = value
};

public static Result<T> Failure(string error) => new() {
IsSuccess = false,
Error = error
};
}

And let’s also have a quick look at how the ShoppingCart class can be used in practice.

static void Main(string[] args)
{
var cart = ShoppingCart.Empty
.AddProduct(new Product("Book", 20m))
.AddProduct(new Product("Pen", 5m));

var maybeDiscounted = cart.ApplyDiscount(0.10m);

if (!maybeDiscounted.IsSuccess)
{
Console.WriteLine($"Invalid discount: {maybeDiscounted.Error}");
}
else
{
var discountedCart = maybeDiscounted.Value!;
Console.WriteLine($"Total: {discountedCart.Checkout():C}");
}
}

The code looks similar with the imperative one with the key difference that it relies on pure functions, which helps us gain the following benefits:

  • Our ShoppingCart class is now immutable (its state does not change, a new object is created for mutations)
  • We do not throw exceptions anymore, and rely on the Result class to highlight a potential failure
  • No hidden state (i.e. no maintenance of total)

Why is immutability important?

The lack of immutability opens up code for thread safety issues. In the imperative implementation of the ApplyDiscount method, we have to be careful that _discount and _total variables are changed atomically, which they’re not. Theoretically one thread could update only _discount while another thread performs a Checkout method call and produces a result over a partially applied operation which would be wrong (and also not the bug I mentioned earlier, sorry).

Why is non-reliance on exceptions important?

Because it’s not obvious. When you are calling public void ApplyDiscount(decimal percentage) you expect to perform a state change, and that’s it. The exception sneaks up on you, which means that in order to understand code like this you have to step into every method (recursively in all of them) in order to understand what can happen.

In our functional example, we return a Result which is a clear indication for the API caller that we may have either a value or a failure and we know we should programmatically deal with a potential failure.

Why is non-reliance on hidden state important?

Well, when reading the Checkout method or the ApplyDiscount method, you also have to be aware of _total . You can’t just read the method and know what it does, you have to know context in which it is called.

The bug I was talking about earlier, was the _discount being applied twice. You can’t know this by reading Checkout , you have to read everything to understand how one thing works.

Small drawbacks like this seem insignificant, but if you’ve ever worked in that maintenance-only project you’d know they add up over time; before you know it you’ll be afraid to change anything because you don’t know what may break (true story).

A glimpse into Monads

We’re changing our shopping cart code a bit using imperative code to make it look more like a real-life coding scenario. We use the same ShoppingCart idea which consists of a couple of validations, computing the final price, applying a discount, validating the final price, and returning the total.

The code below is simple and it’s meant to feel very familiar, especially this check/return flows the iterations and validations, the getting of a value to compute another value.

public static class ImperativeCartProcessor
{
public static decimal ProcessCart(ShoppingCart cart)
{
if (cart.Items == null)
throw new InvalidOperationException("Cart is invalid");

if (cart.Items.Count == 0)
throw new InvalidOperationException("Cart is empty");

decimal total = 0m;
foreach (var product in cart.Items)
{
if (product != null && product.Price > 0)
{
total += product.Price;
}
}

total = total * 0.9m;

if (total <= 0)
throw new InvalidOperationException("Total is zero or negative");

return total;
}
}

We can spot a very common pattern of “if X happens then exit”, or “if X happens then do Y” as well as taking a value and converting it to another value. All these operations translate very neatly in functional programming and we’ll be showcasing that by extending the Result class.

We can reason about a Result<T1> that it could be transformed to another Result<T2> either as a Success<T2> or Failure<T2> . With this idea, we can very easily translate a few repetitive imperative code operations to Result transformations:

  • Apply a “Map” transformation Func<A, B> such that a Result<A> would be converted to a Result<B> ; this way we can take for example a Cart and convert it to a decimal total price
  • Apply a “Filter” dynamic conditions using Func<A, bool> such that a Result<A> may become either a Success<A> or a Failure<A> .
  • Apply a “FlatMap” transformations using functions such as Func<A, Result<B>> that allow us to compose results, such that a Result<A> can become a Result<B> ; we’re basically taking 2 results and generating a final Success<B> or Failure<B> result.

I’m sharing below the Map / Filter / FlatMap implementations for our Result class.

public class Result<T>
{
public bool IsSuccess { get; }
public T? Value { get; }
public string? Error { get; }

private Result(T? value, bool isSuccess, string? error)
{
Value = value;
IsSuccess = isSuccess;
Error = error;
}

public static Result<T> Success(T value) => new Result<T>(value, true, null);
public static Result<T> Failure(string error) => new Result<T>(default, false, error);

public Result<U> Map<U>(Func<T, U> func) => IsSuccess
? Result<U>.Success(func(Value!)) // new result with function return
: Result<U>.Failure(Error!); // or keep existing error

public Result<U> FlatMap<U>(Func<T, Result<U>> func) => IsSuccess
? func(Value!) // our success becomes another result
: Result<U>.Failure(Error!); // convert to U but with existing error

public Result<T> Filter(Func<T, bool> predicate, string errorMessage) =>
IsSuccess
? (predicate(Value!)
? this // keep current result
: Failure(errorMessage)) // switch to Failure
: this; // maintain our Failure if we're already !IsSuccess
}

Let’s see how we can use this new defined conversion methods in order to convert the previously imperative solution in a declarative functional solution.

public static class FunctionalCartProcessor
{
public static Result<decimal> ProcessCart(ShoppingCart cart)
{
return Result<ShoppingCart>.Success(cart)
.Filter(c => c.Items != null, "Cart is invalid")
.Filter(c => c.Items.Count > 0, "Cart is empty")
.Map(c => c.Items.Where(item => item != null)
.Where(item => item.Price > 0)
.Sum(p => p.Price))
.FlatMap(total => ApplyDiscount(total, 0.1m))
.Filter(total => total > 0, "Total is zero");
}

private static Result<decimal> ApplyDiscount(decimal total, decimal discount)
{
var discounted = total * (1 - discount);

return discounted > 0
? Result<decimal>.Success(discounted)
: Result<decimal>.Failure("Discounted total is zero");
}
}

Comparing between the two solutions we can see clearly that the functional one is based on declaring what needs to happen rather than micro-managing it every step of the way. We maintain our pure functions approach making the solution immutable, with clear intent, and easy to follow due to no side effects or state changes.

Yes — we did have to use an extra utility class called Result but it is an investment that has a one time cost with numerous applications within a code-base.

These sort of operations involving a wrapper are examples of a common pattern in functional programming called a “monad”. In our specific case, Result is a monad because it implements a minimal set of monadic combinators (Unit via Success / Failure and FlatMap) that satisfy the basic laws of associativity and identity.

// law of associativity
(m.FlatMap(f)).FlatMap(g) == m.FlatMap(x => f(x).FlatMap(g))

// law of left-identity => Unit(x).FlatMap(f) == f(x)
Result<int>.Success(5).FlatMap(x => Result<int>.Success(x*2)); // same as f(5)

// law of right-identity => m.FlatMap(Unit) == m
var m = Result<int>.Success(5);
m.FlatMap(Result<int>.Success); // flatmap unit same as m

I will let you think about why the law of associativity is also true for our Result class. We will not go into further depth as monads are not the core focus of this article, and they deserve a full discussion of their own.

I just wanted you to have a full glimpse into the functional programming world. There is no need to worry about associativity law right now. The key point here is that you may create wrappers and apply transformations over the underlying values.

In conclusion

We went through quite a few functional programming fundamentals, yet in a very practical and simple way. Although at first it may have seemed daunting you should now be familiar with:

  • Higher order functions
  • Pure functions, referential transparency, side-effects, immutability
  • A bit of monadic pattern thinking

We also compared between functional and non-functional code, and although the imperative will always feel more familiar we can also relate to the clean-ness of functional concepts and how they make our lives objectively easier.

We even touched briefly on functional API design and created the Result class that helped us create an API with clear and predictable intentions.

Finally, we took Result further using monads and chained various transformations, a very powerful functional programming design pattern that is also the fundamental principle of LINQ.

I genuinely hope you’ve found something here that you can take and apply in your day-to-day work! Functional programming isn’t just theory — it’s a set of practical tools that can make your C# code simpler, more reliable, and therefore easier to maintain.


Functional Programming Basics Every C# Developer Can Use Today was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Level Up Coding - Medium and was authored by Cristian Toader


Print Share Comment Cite Upload Translate Updates
APA

Cristian Toader | Sciencx (2025-11-19T17:47:58+00:00) Functional Programming Basics Every C# Developer Can Use Today. Retrieved from https://www.scien.cx/2025/11/19/functional-programming-basics-every-c-developer-can-use-today-2/

MLA
" » Functional Programming Basics Every C# Developer Can Use Today." Cristian Toader | Sciencx - Wednesday November 19, 2025, https://www.scien.cx/2025/11/19/functional-programming-basics-every-c-developer-can-use-today-2/
HARVARD
Cristian Toader | Sciencx Wednesday November 19, 2025 » Functional Programming Basics Every C# Developer Can Use Today., viewed ,<https://www.scien.cx/2025/11/19/functional-programming-basics-every-c-developer-can-use-today-2/>
VANCOUVER
Cristian Toader | Sciencx - » Functional Programming Basics Every C# Developer Can Use Today. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/11/19/functional-programming-basics-every-c-developer-can-use-today-2/
CHICAGO
" » Functional Programming Basics Every C# Developer Can Use Today." Cristian Toader | Sciencx - Accessed . https://www.scien.cx/2025/11/19/functional-programming-basics-every-c-developer-can-use-today-2/
IEEE
" » Functional Programming Basics Every C# Developer Can Use Today." Cristian Toader | Sciencx [Online]. Available: https://www.scien.cx/2025/11/19/functional-programming-basics-every-c-developer-can-use-today-2/. [Accessed: ]
rf:citation
» Functional Programming Basics Every C# Developer Can Use Today | Cristian Toader | Sciencx | https://www.scien.cx/2025/11/19/functional-programming-basics-every-c-developer-can-use-today-2/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.