Functional Programming - Part 1
This article introduces the concept of functional programming and why it’s useful. One of the biggest challenges in software development is complexity. Changes are inevitable, especially when adding new features. These changes can make the code harder to understand, increase development time, and introduce bugs. Moreover, it’s almost impossible to change something in a program without causing side effects or unwanted behavior. All of this can slow down development and even cause project failure.
Imperative programming styles, like object-oriented programming, can help manage complexity—if done right. Abstraction in OOP hides complexity, but there are still challenges when dealing with threads and performance.
The Problem
C# developers are familiar with object-oriented programming (OOP). We often focus on how to use inheritance, encapsulation, and polymorphism to design our code. However, in OOP, multiple threads might access the same memory area, causing race conditions. While we can write thread-safe code, improving performance can be challenging, especially when refactoring existing code for parallel execution.
The Solution: Functional Programming
Functional programming (FP) is a paradigm that dates back to before computers, originating from a mathematical theory called lambda calculus. It allows functions to calculate results without changing the program’s state, which makes the code simpler to understand, reduces side effects, and makes testing easier.
Functional Programming Languages
Languages like Lisp, Clojure, Erlang, and Haskell support functional programming. F# is a language in the .NET ecosystem that also embraces FP principles. Interestingly, general-purpose languages like C# are flexible enough to support various paradigms, including functional programming. Since many of us use C# for development, integrating functional programming concepts can help solve some of our problems.
Key Concepts
In functional programming, a function is similar to a mathematical function: it takes specific inputs and returns the expected output without changing the state. This concept is called referential transparency and function honesty.
Note that in C#, a function is not just a method; Func
, Action
, and Delegate
are also types of functions.
Referential Transparency
Referential transparency means that by looking at a function’s inputs and its name, you should be able to predict what it does. A function should only depend on its inputs and should not be affected by global state. For example:
1
2
3
4
5
public int CalculateElapsedDays(DateTime from)
{
DateTime now = DateTime.Now;
return (now - from).Days;
}
This function is not referentially transparent because it depends on the global DateTime.Now
, which changes over time. To fix it, pass DateTime.Now
as an argument:
1
public static int CalculateElapsedDays(DateTime from, DateTime now) => (now - from).Days;
Now the function is referentially transparent because it no longer depends on a global variable.
Function Honesty
A function should handle all possible inputs and outputs correctly. For example:
1
2
3
4
5
public int Divide(int numerator, int denominator)
{
return numerator / denominator;
}
Is this function transparent? Yes, but it doesn’t cover all cases. If the denominator is zero, it will throw an error:
1
2
var result = Divide(1, 0); // Divide by zero error
To solve this, we can prevent division by zero by using a custom type:
1
2
3
4
public static int Divide(int numerator, NonZeroInt denominator)
{
return numerator / denominator.Value;
}
Here, NonZeroInt
is a custom type that only accepts values other than zero.
Functions as First-Class Values
In functional programming, functions are considered first-class values. This means functions can be assigned to variables and passed as arguments to other functions. For example:
1
2
3
Func<int, bool> isEven = x => x % 2 == 0;
var list = Enumerable.Range(1, 10);
var evenNumbers = list.Where(isEven);
In this case, isEven
is a function assigned to a variable and passed to Where. This ability is crucial for using higher-order functions.
Higher-Order Functions (HOF)
A higher-order function is a function that accepts other functions as arguments or returns a function as a result. The Where method in LINQ is a good example:
1
2
3
4
5
6
public static IEnumerable<T> Where<T>(this IEnumerable<T> ts, Func<T, bool> predicate)
{
foreach (T t in ts)
if (predicate(t))
yield return t;
}
In this example, Where
takes a function (predicate
) and applies it to each item in the list. The function passed to Where
is a higher-order function.
Pure Functions
Pure functions are essentially mathematical functions that follow the two concepts we’ve already discussed: referential transparency ** and **function honesty
A pure function should never have side effects. This means it shouldn’t modify global state or use global variables as inputs. Pure functions are easy to test because for a given input, they always return the same output. The order of execution doesn’t matter! These are the core components of a functional program and can be used for parallel execution, lazy evaluation, and memoization.
In the next articles of this series, we will explore functional programming implementations and common patterns in C#.