Dataslope logoDataslope

Modern C# as a Functional Language

A version-by-version tour of the C# features that made functional-style code idiomatic, ending with the modern toolset you'll use for the rest of the course

C# started life in 2002 as a deliberate response to Java, with the same OO-centric flavor. Over twenty years it absorbed an enormous amount from F#, Haskell, Scala, and TypeScript — to the point where modern C# is comfortably a multi-paradigm language with first-class functional support.

This page is a short, opinionated tour of the features that matter for functional thinking, in roughly the order they appeared. You do not need to memorize the version numbers. You do need to recognize the syntax — every page after this will use them freely.

Timeline

We'll spend the rest of this page on the features highlighted above.

Lambda expressions (C# 3.0)

A lambda is an inline function. It is the unit of behavior you pass to LINQ.

Code Block
C# 13

A lambda captures variables from its enclosing scope. This is called a closure.

Code Block
C# 13

Captured variables are a common source of bugs in loops. The general rule is to capture immutable data; we'll come back to this.

Iterators with yield (C# 2.0)

yield return lets a method produce a sequence one element at a time, lazily. It's the secret behind almost every LINQ operator.

Code Block
C# 13

Notice the interleaving: the producer runs only as the consumer asks for the next value. We'll come back to this in Deferred Execution.

Expression-bodied members (C# 6/7)

These let you write one-liner methods and properties without { } and return.

Code Block
C# 13

Expression bodies make short pure functions look like the math they implement.

Pattern matching and switch expressions (C# 7/8)

A switch expression returns a value. Combined with patterns, it is the closest thing C# has to Haskell-style pattern matching.

Code Block
C# 13

Switch expressions are expressions — they return a value, which means they compose. You can use one anywhere you'd use an expression: inside Select, as a method body, as an argument.

Records (C# 9, struct in C# 10)

A record is a class designed for value semantics: equality by content, easy non-destructive update, ToString that prints fields. Records are the workhorse of immutable data in modern C#.

Code Block
C# 13

with produces a new record with some fields replaced. The original is untouched. This is immutable update.

Local functions (C# 7)

A local function is a method declared inside another method. They are great for splitting a pipeline into named steps without polluting the class.

Code Block
C# 13

Local functions can be static (recommended when they don't capture outer variables) — this prevents accidental closure-related bugs.

Tuples and deconstruction (C# 7)

Tuples are ad-hoc, lightweight value types — perfect for returning multiple values without inventing a class.

Code Block
C# 13

Collection expressions (C# 12)

The new [ ... ] literal works for arrays, lists, spans, and more — the compiler picks the right type.

Code Block
C# 13

The modern functional toolkit, summarized

By 2024-era C#, "functional" idioms look like this:

Code Block
C# 13

Every page after this one uses some subset of these features. With the historical setup complete, we can leave history behind and start building the functional toolkit from first principles — beginning with the most important idea of all: pure functions.

QuestionSelect one

What does the C# expression p with { Age = 37 } do, when p is a record?

It mutates p so that its Age becomes 37 and returns p.

It creates a new record value that is a copy of p but with Age set to 37, leaving p unchanged.

It throws a compile error because records do not support partial updates.

It modifies p only if p is a mutable record (i.e. not declared with init-only properties).

On this page