Functional features of C#
C# is an object-oriented language at its core, but it also has very good support for some functional programming techniques.
Immutable types
One of the core principles of FP is immutable data, i.e. objects that cannot be modified (change their state) after they have been created. Every time a variable changes, a new one must be created. In C# creating user defined types that are immutable is possible, but it is far from as easy as in functional language.
public class Money
{
public decimal Value { get; }
public Money(decimal value) => Value = value;
}
The Money type is immutable and it declares a getter-only auto-property. The backing field of Value is implicitly declared as readonly. As a result it can be initialized only in the constructor (example above) or inline:
public decimal Value { get; } = 12.34m;
To change a property of Money object, a new object must be created.
public static Money Add(Money money, decimal value) => new Money(money.Value + value);
Function delegates
A delegate is a type that safely encapsulates a method. Delegates are type safe, and secure. C# 3.0 includes built-in generic delegate types Func and Action, so that you don't need to define custom delegates.
Func delegates are a light weight version of doing function delegates in C#. We see them used in LINQ very often. We can create a function that we can pass around like a variable. Func has zero or more input parameters and one output parameter. The last parameter is the out parameter.
Func<int> multiplyByTwo = x => x * 2;
Func<int, int, int> add = (x, y) => x + y;
Func<int, bool> isEven = n => n % 2 == 0;
Higher order functions
Higher order functions are functions that take other functions as inputs or return a function as output, or both. Almost every LINQ function is an HOF.
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;
}
Read more about higher-order functions.
Expressions instead of statements
An expression produces a value without mutating state and can be written wherever a value is expected. Statements define an action and are executed for their side effects.
Both of the following examples produce the same result, but the expression one doesn't change state.
// statement
public static string GetGreeting(int hour)
{
string greeting;
if (hour < 12)
greeting = "Good Morning";
else
greeting = "Good Afternoon";
return greeting;
}
// expression
public string GetGreeting(int hour) =>
hour < 12 ? "Good Morning" : "Good Afternoon";
Read more about expressions vs statements.
Method chaining
In FP we see a lot of pipelining. C# does not have a pipe command, so we rely on method chains. StringBuilder is the most common .NET example:
public static string GetHtmlButton(string value) => new StringBuilder()
.Append("<button ")
.Append("type=\"submit\" ")
.Append($"value=\"{value}\"")
.Append(">")
.ToString();
Extension methods
Those pipelines are often build using extension methods. They allow you to add a method onto an existing object and give it additional functionality from outside of the class. LINQ methods are extension methods.
public static StringBuilder AppendWhen(this StringBuilder sb, string value, bool predicate) =>
predicate ? sb.Append(value) : sb;
Yield
In the following code we are creating a new list and then we are adding elements to that list. We can do the same thing with the yield operator, by yielding those out instead of building a new list of integers and returning that list. In this way we eliminated that List<int> list = new List<int>().
// without yield
public static IEnumerable<int> GetEvenNumbers(int[] numbers)
{
List<int> list = new List<int>();
foreach (var num in numbers)
{
if (num % 2 == 0) list.Add(num);
}
return list;
}
// with yield
public static IEnumerable<int> GetEvenNumbers(int[] numbers)
{
foreach (var num in numbers)
{
if (num % 2 == 0) yield return num;
}
}
LINQ
LINQ is a functional library that offers implementations for many common operations on sequences (IEnumerable). For example:
Enumerable.Range(1, 20)
.Where(x => x % 2 == 0)
.OrderBy(x => x)
.Select(x => x * 2);
Fundamental functions in FP are map, filter and sort.
-
Mapping:
The map operation takes a sequence of elements, applies a transformation function to each one of them, and returns a new sequence with resulting elements. C#/LINQ implements map operation with Select extension method. The type of the resulting sequence doesn't need to match the type of the source sequence.
IEnumerable<int> userIds = GetUsers().Select(x => x.Id);
-
Filtering:
The filter operation creates new sequence containing just the elements that pass some condition. In LINQ we use Where.
IEnumerable<int> odd = GetNumbers().Where(x => x % 2 == 0);
-
Sorting:
Sorting gives a new sequence ordered according to some key. In LINQ we use OrderBy and OrderByDescending.
IEnumerable<int> increasing = GetNumbers().OrderBy(x => x);
Tuples
Tuple types are heavily used in functional programming. C# 7 introduced ValueTuple, which is a value type representation of the tuple object and it has new syntax and features.
public (string name, int age) GetPerson() => (name: "John", age: 20);
(string name, int age) person = GetPerson();
Tuples are value types, and their elements are public, mutable fields.
Local functions
In functional programming, functions are the core building blocks. We work with lots of simple functions. In C# 7 we can declare functions inside other function bodies. This is useful when a function makes sense only inside of a single method that uses it.
static void Main()
{
int Sum(int x, int y) => x + y;
Console.WriteLine(Sum(5, 10));
}
In this example, the Sum method is defined in the body of Main and it can be used only inside that method.
Resources: