avatarMatthew MacDonald

Summary

C# 9 introduces functional programming features such as records for immutability and expressions for declarative logic, signaling a shift towards a multiparadigm language that blends object-oriented and functional programming principles.

Abstract

The latest iteration of C#, version 9, is poised to integrate more functional programming concepts into the language, which has traditionally been object-oriented. The introduction of records provides a means to create immutable data structures, ensuring data integrity and preventing side effects. Additionally, the new expressions feature, particularly switch expressions, offers a more declarative approach to conditional logic, enhancing code clarity and maintainability. These enhancements reflect a broader trend within C# to embrace a multiparadigm approach, similar to C++, allowing developers to leverage functional patterns within a language that was once purely object-oriented. The evolution of C# suggests a maturation of the language, offering developers a richer set of tools and paradigms to choose from based on their specific needs.

Opinions

  • Mads Torgersen, the lead C# language designer, expresses interest in functional programming and suggests that if he were to redesign C#, he would aim for a balance between object-oriented and functional paradigms from the outset.
  • The article implies that the influence of functional programming at Microsoft is more than a passing trend, as evidenced by the deliberate inclusion of functional features in C# 9.
  • The author posits that the new features in C# 9, such as records and expressions, are significant and not merely superficial additions, indicating a substantial evolution in the language's capabilities.
  • There is an acknowledgment that while C# will remain an object-oriented language at its core, the integration of functional features has reached a critical mass, transforming C# into a versatile, multiparadigm language.
  • The author suggests that the full utilization of C#'s newfound flexibility, including its functional aspects, will depend on the adaptability and willingness of developers to embrace these changes.

C# 9 Creeps Closer to Functional Programming

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

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:

“C# started out very object-oriented, and has been adopting functional features over time. Starting over, I would probably try to strike a balance between the two from the get-go.”

(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.

Programming
Csharp
Functional Programming
Oop
Microsoft
Recommended from ReadMedium