Advanced Delegates and Functional Programming
C# is multi-paradigm: besides objects it supports a strong functional style.
Concept
C# is multi-paradigm: besides objects it supports a strong functional style.
Functions as values Func<>, Action<>, lambdas, method groups Pure functions
Same input → same output, no side effects: easy to test and parallelize Higher-order functions
Take/return functions: Scale(3.6) returns a function, LINQ takes functions Compose(f, g) → x => g(f(x)) Currying / partial application limit => value => value > limit
record, init, readonly struct, System.Collections.Immutable Declarative data processing
LINQ: Where (filter), Select (map), Aggregate (reduce) Pattern matching
Switch expressions over records instead of virtual methods Algebraic data types
Closed record hierarchies (Result<T> = Ok or Error) Cache results of pure functions
Variance: Func<out TResult> is covariant, Action<in T> is contravariant — a Func<string> can be used as a Func<object>, an Action<object> as an Action<string>.
Multicast: += builds an invocation list; GetInvocationList() exposes it. For Func<>, only the last return value is kept.
Closures capture variables, not values (Module 18).
Local functions and static lambdas avoid unintended captures and allocations.
Event memory management: an event keeps every subscriber alive (Module 62 shows the leak). Weak events hold subscribers through WeakReference so they can be collected (WPF's WeakEventManager); otherwise always unsubscribe with -=.
Event aggregators (a mediator for events: publishers and subscribers only know the aggregator and the message type) decouple components in desktop apps and plugin systems — Module 63 contains one.
Why It Matters
Signal-processing pipelines, rule engines and data transformations are naturally functional: input → transform → output. Immutable state is thread-safe by construction (Modules 33–35), and pure functions are trivially unit-testable.
Syntax
static Func<double, double> Scale(double f) => x => x * f;
Func<double, Func<double, bool>> above = limit => v => v > limit;
var next = state with { Soc = state.Soc - 0.1 };
abstract record Result<T>; record Ok<T>(T Value) : Result<T>; record Error<T>(string Message) : Result<T>;Example
44.1 Functional techniques in C#
using System.Collections.Immutable;
// Pure functions: same input -> same output, no side effects
static double Kmh(double mps) => mps * 3.6;
// Higher-order functions: take or return functions
static Func<double, double> Scale(double factor) => x => x * factor;
static Func<T, TResult> Compose<T, TMid, TResult>(Func<T, TMid> f, Func<TMid, TResult> g) => x => g(f(x));
var toKmh = Scale(3.6);
var round = (double x) => Math.Round(x, 1);
var pipeline = Compose(Compose(toKmh, round), (double kmh) => $"{kmh} km/h");
Console.WriteLine($"pure: {Kmh(25)}, composed pipeline(27.78 m/s) = {pipeline(27.78)}");
// Currying and partial application
Func<double, Func<double, bool>> above = limit => value => value > limit;
Func<double, bool> overheat = above(105);
Console.WriteLine($"curried overheat(110) = {overheat(110)}");
// Immutability: records + immutable collections; "modification" returns new values
var state = new BatteryState(0.80, ImmutableList<string>.Empty);
var after = Discharge(Discharge(state, 0.30), 0.40);
Console.WriteLine($"original unchanged: {state.Soc:P0}; after two discharges: {after.Soc:P0}, events: {string.Join(" | ", after.Events)}");
// Declarative data processing w
…Expected Result
state still shows 80% after two discharges: Discharge returned new records rather than mutating. The Result<int> type turned the invalid address into a value instead of an exception, and memoization made the naive recursive fib(80) instant.
pure: 90, composed pipeline(27.78 m/s) = 100 km/h
curried overheat(110) = True
original unchanged: 80%; after two discharges: 10%, events: -30% | -40%
average of low readings: 11650 mV
total area: 9.14
0x7E0 -> ok 0x7E0
banana -> error: 'banana' is not a hex address
memoized fib(80) = 23416728348467685
handled text
ABC
invocation list: 3 delegatesCommon Mistakes
Hidden side effects inside LINQ lambdas (modifying outer variables in Select).
Over-abstracting simple code with deep function composition.
Memoizing impure functions (results depend on time, I/O or mutable state).
Assuming with deep-copies nested mutable objects.
Relying on the return value of a multicast Func<>.
Best Practices
Keep core logic pure; push I/O to the edges ("functional core, imperative shell").
Use records and immutable collections for shared state.
Use Result-style types for expected failures and exceptions for exceptional ones.
Prefer LINQ for readability; switch to loops only where profiling shows a need.
Interview Relevance
Medium: "what is a pure function?", "benefits of immutability?", "what is currying?", "covariance of Func<>?", "how do you model success/failure without exceptions?".
Practice Questions
Write Pipe<T>(T value, params Func<T, T>[] steps) and use it to scale, offset and clamp a sensor value.
Implement Map and Bind for Result<T>.
Replace a mutable class Counter with an immutable record and a pure Increment function.
Memoize an expensive CRC table computation.
Quick Quiz
Q1. (Multiple choice) Which is a pure function? A) double Kmh(double mps) => mps * 3.6; B) int Next() => ++_counter; C) DateTime Now() => DateTime.Now; D) void Log(string s) => Console.WriteLine(s); Answer: A.
Q2. (Predict the output) Action multi = () => Console.Write("A"); multi += () => Console.Write("B"); multi += () => Console.WriteLine("C"); multi();
Answer: ABC, and the invocation list has 3 delegates (verified).
Q3. (True/False) Action<object> can be assigned to a variable of type Action<string>.
Answer: True — Action<in T> is contravariant (verified).
Q4. (Find the bug) int count = 0; var evens = data.Where(x => { count++; return x % 2 == 0; }); — count is 0 after the line.
Answer: LINQ is lazy; the lambda runs only when evens is enumerated — and side effects in queries are a code smell.
Coding Challenge
Write a Pipe helper that applies a sequence of Func<double, double> steps, and use it to convert a raw sensor value 2048 to volts (× 5/4096), add an offset of 0.1, and clamp to 0..5.
double result = Pipe(2048.0,
raw => raw * 5.0 / 4096,
v => v + 0.1,
v => Math.Clamp(v, 0, 5));
Console.WriteLine($"{result:F3} V");
static T Pipe<T>(T value, params Func<T, T>[] steps) =>
steps.Aggregate(value, (acc, step) => step(acc));