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 delegates

Common 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));