C# 9 Creeps Closer to Functional Programming

What happens when a successful OO language cross-pollinates with FP ideas?

Matthew MacDonald
Sep 28 · 7 min read
Image for post
Image for post

There are programming languages, and there are the ways we use them. Sometimes the two line up, and sometimes our expectations and conventions obscure the full range of possibilities.

For example, many programmers who’ve never touched a line of C++ think of it as an object-oriented version of the ancient C language. And they’re not entirely wrong (after all, C++ is a direct evolution of an experiment called C with classes). And yet, any modern-day developer who actually uses C++ knows that it’s a wide-open multiparadigm language that can go in plenty of different directions.

C# is a different story — or is it? It started out as a streamlined, Java-influenced, thoroughly object-oriented language. (For a brief time, it was even codenamed Cool, for “C-like Object Oriented Language.”) But recently, the ground has been shifting. Among the many improvements in C# 9 — the version we’ll see released this year with .NET 5 — are some features that have the distinct flavor of functional programming.

Mads Torgersen, the lead C# language designer has been clear about his interest in functional programming. When asked what he would do if he had the chance to redesign C# from scratch, he said:

(Side note: if you were expecting us to talk about Anders Hejlsberg, the creator of C#, let us update you. Hejlsberg is still active at Microsoft, but now spends most of his days heading up development on the TypeScript language.)

Clearly, the ideas of functional programming are exerting an influence at Microsoft. But is functional programming in C# a promising direction or a mere flash in the pan? Let’s take a look at the two most notable examples in C# 9.

Records: All the data, none of the mutability

One of the key ideas in functional programming is that data structures should be immutable. You create packages of information, confident that they can’t be changed as they travel around your code.

If you’re used to object-oriented programming, where every object is a bucket of hidden variables, this mindset takes some getting used to. But the advantages are significant. Imagine a world where inconsistent state is impossible, and unexpected side effects never happen. (If you’re wondering what I mean by side effects, here’s an example: you call a method on an object that, unknown to you, changes that object’s state. This change then slips through to somewhere else in your program, where it does who knows what.)

C# 9 takes a big step forward on immutability with a new record feature that lets you build objects that can’t change. The word record hints at one common use case — representing records from a database.

Here’s an example, with the new bits in bold:

public record Employee
{
public string Name { get; init; }
public decimal YearlySalary { get; init; }
public DateTime HiredDate{ get; init; }
}

The init keyword is another new ingredient in C# 9. It lets you define a property that can only be set during object creation, either through a constructor (which the Employee class doesn’t have) or an object initializer, like this:

var theNewGuy = new Employee
{
Name = "Dave Bowman",
YearlySalary = 100000m,
HiredDate = DateTime.Now()
};

After this point, there’s no way to change any of the properties of theNewGuy object.

Now, there are obviously plenty of reasons you might want to create a modified version of theNewGuy object in the course of an application. For example, maybe you want to give this employee a promotion and commit a new version of the record back to the database. To do that, you could construct a new Employee object and copy the important details over. But C# 9 has a more elegant solution using the with keyword. It gives you a concise way to create a new record using some of the details from another instance:

var promotedGuy = theNewGuy with { YearlySalary = 150000m }; 

Now, promotedGuy has the same values for Name and HiredDate as theNewGuy, but with a modified YearlySalary. Incidentally, the copying that with does is optimized for efficiency, because it can take advantage of the fact that the shared data will never change.

Records have some additional conveniences. They’re compared by value, not instance. That means if you have two records with the same data, they are deemed equal:

// Make a new record.
var yetAnotherGuy = promotedGuy with { YearlySalary = 100000m };
// Even though these records are different instances (in object
// speak), they have all the same data, and so they are considered
// equal.
if (theNewGuy == yetAnotherGuy)
{
// You end up here.
}

You could create an immutable class on your own, but it’s not quite as easy as you’d expect. You need to think about how records are initialized, compared, and copied. There’s a lot of boilerplate to write. But the record feature makes immutable objects feel like an effortless part of the language.

Expressions: Conditional logic, but 100% declarative

Another key idea in functional programming is that expressions are safer than control flow logic like loops. For example, let’s say you want to total up values in a collection. To keep a functional programmer happy, you’d use a reducer function that acts on every item in the collection. To make a functional programmer angry, you would iterate over the collection in a loop. (If you’re familiar with databases, this is the difference between a SQL query and a cursor that steps through a recordset. Functional programming prefers the query.)

The goal is to make code clearer, more testable, and — once again — immune to unexpected side effects. Who’s to say that code that’s iterating through your collection won’t change something? Who knows what really goes on in that loop?

In C#, there’s been a slow shift from imperative to declarative code with expressions. They first started to work their way into the language with LINQ in C# 3. Back then, many developers treated expressions as little more than an exotic feature for performing searches and querying data sources. But things changed in C# 7, 8, and now 9, with the steady refinement of switch expressions, a declarative alternative to conditional code.

Switch expressions hijack the old-fashioned switch branching keyword, but give it a completely new syntax. In fact, switch expressions have evolved to be so different from the classic switch statement that the shared keyword is little more than a potential point of confusion.

Instead of executing different blocks of conditional code, switch expressions use patterns to transform starting data into a final result. The best way to get an overall idea of how they work is to take a look at an example, like this CalculateToll() method adapted from Microsoft’s pattern matching tutorial:

This function accepts an object, performs a calculation based on the data in this object, and returns a result: the toll that needs to be charged for a certain type of vehicle. In traditional imperative code, you’d make this calculation using conditional logic and some temporary variables. But with expressions, the expression that evaluates the data also leads to the result.

The first order of business for this switch expression is to determine the type of object. If it’s a Car, a series of patterns map out the possibilities based on the Car.Passengers property. For example, cars with two passengers get a $0.50 discount off the base $2.00 rate:

Car { Passengers: 2}    => 2.00m - 0.50m,

Similar logic kicks in for Taxi objects. But with DeliveryTruck objects, the calculation revolves around the DeliveryTruck.GrossWeightClass property. This is handled by a nested switch, which adds a surcharge for weights over 5000 pounds, and a discount for weights under 3000 pounds. Everyone else gets the base rate, with the help of the _ operator. This is called the discard pattern, and it kicks in when no other pattern makes a match).

DeliveryTruck t when t.GrossWeightClass switch
{
> 5000 => 10.00m + 5.00m,
< 3000 => 10.00m - 2.00m,
_ => 10.00m,
},

If you fall through all these cases without matching a pattern, you have a type that isn’t recognized. There’s a final discard pattern for that:

_ => throw new ArgumentException("Not a known vehicle type")

If you leave this part out, C# will throw an exception for you, but this is your chance to supply more information in the exception object. Even better, the C# compiler will warn you when you build your program if your patterns don’t cover all the possible cases.

There are many more ways to write patterns, and you can learn about them from Microsoft’s pattern matching tutorial. But expect slightly clunkier syntax, because it’s not yet updated to use the C# 9 refinements.

Where does this leave us?

C# will never be a functional-first language. But it’s been quietly accumulating functional features for several versions. With C# 9, it feels like we’ve reached a tipping point. The functional features are no longer frills around the edges, but a core feature area that you can use to implement functional patterns in your codebase. Which also means that C# is shifting into something different as well, going from a single-concern OO language to a multiparadigm language like C++. Whether developers will take full advantage of this flexibility remains to be seen.

For a once-a-month email with our best tech stories, subscribe to the Young Coder newsletter.

Young Coder

Hack. Code. Think. Stories about science, tech, and programming.

Matthew MacDonald

Written by

Teacher, coder, long-ago Microsoft MVP. Author of heavy books. Join Young Coder for a creative take on science and technology. Queries: matthew@prosetech.com

Young Coder

Hack. Code. Think. Stories about science, tech, and programming.

Matthew MacDonald

Written by

Teacher, coder, long-ago Microsoft MVP. Author of heavy books. Join Young Coder for a creative take on science and technology. Queries: matthew@prosetech.com

Young Coder

Hack. Code. Think. Stories about science, tech, and programming.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store