Dive C2.06.1
One method, one function
A comparer is a function. A callback is a function. The interface was the wrapping.
In C#, to vary behaviour you name an interface and write a class that implements it. For the
everyday case — a comparer, a strategy, a callback — that interface has exactly one method, and the class exists
only to carry that method to where it runs: IComparer<T> for a sort,
Func<T,R> for a transform, an event handler for a callback. F# names the method
directly. A string -> string -> int is the comparer, and it is passed
straight to List.sortWith with no class declared and no container to inject it. The
one-method interface and the function are the same abstraction; F# spells it without the type.
The interface that holds one method
Look at the interfaces a C# program reaches for most. IComparer<T> declares one
method, Compare. A strategy holds one method, the operation to vary. A callback is
one method, invoked later. Each is a contract for a single behaviour, and in C# a class supplies that behaviour
and is then passed or injected. The class is a container for one function.
F# takes the function and drops the container. A comparison is a value of type
string -> string -> int; a transform is an int -> int;
a callback is whatever its signature says. Each is named with let or written inline as
a lambda, and passed where the behaviour is wanted. The signature of byLength below is
the whole contract.
let byLength (a: string) (b: string) = compare a.Length b.Length // val byLength : string -> string -> int List.sortWith byLength ["ccc"; "a"; "bb"] // val it : string list = ["a"; "bb"; "ccc"]
- byLength
- a comparison function —
string -> string -> int, returning negative, zero, or positive. This signature is the entireIComparer<T>contract. - sortWith
- takes a comparison function and a list; it is the F# call that, in C#, takes an
IComparer<T>or a lambda. - compare
- the built-in structural comparison;
compare a.Length b.Lengthorders by length without writing the sign logic by hand. - no class
- there is no
class ByLength : IComparer<string>— the function is the value the sort consumes.
The C# the reader knows, beside the F#
C# already lets you hand a comparison to Sort as a lambda — the
Comparison<T> overload — so the inline case is close. The difference shows when
the behaviour is named and reused: C# reaches for a class implementing
IComparer<T>, while F# names a function with let and passes
it. A callback is the same story — C# types it Func<int,int>, F# types it
int -> int.
C# — a comparison lambda, or a named comparer class
var ls = new List<string> { "ccc", "a", "bb" }; ls.Sort((a, b) => a.Length.CompareTo(b.Length)); // ls is now: a, bb, ccc // to reuse the behaviour, a named one-method class: class ByLength : IComparer<string> { public int Compare(string a, string b) => a.Length.CompareTo(b.Length); }
F# — one function value, named and passed
let byLength (a: string) (b: string) = compare a.Length b.Length // val byLength : string -> string -> int List.sortWith byLength ["ccc"; "a"; "bb"] // val it : string list = ["a"; "bb"; "ccc"]
A function that takes a function makes the same point from the other side. C# types the parameter
Func<int,int>; F# types it int -> int and applies it by
writing f x. The higher-order function is where strategies, mappers, and callbacks all
arrive — as values, not as injected services.
let apply (f: int -> int) x = f x // val apply : (int -> int) -> int -> int apply (fun n -> n * 2) 10 // val it : int = 20
Swap the strategy
One fixed list, ["delta"; "a"; "echo"; "bc"]. Pick the function passed to the
operation: two comparisons handed to List.sortWith (by length, by alphabet) and one
transform handed to List.map (uppercase). The "interface" never changes — what changes
is the single function value you choose. The readout shows the exact FSI result for the chosen strategy.
the chosen function · applied to the fixed list
| the function | call | result |
|---|---|---|
| byLength | List.sortWith byLength | ["a"; "bc"; "echo"; "delta"] |
| byAlpha | List.sortWith byAlpha | ["a"; "bc"; "delta"; "echo"] |
| toUpper | List.map toUpper | ["DELTA"; "A"; "ECHO"; "BC"] |
The pipeline is fixed; the function in the slot is the only thing chosen. In C# that slot is an
injected IComparer<T> or Func — in F# it is a value, named
where it is read.
What the function value removes
The gain is not a shorter spelling of the Strategy pattern. It is that the abstraction the pattern was reaching for — behaviour passed as a value — is built into the type system. There is no interface to declare, no class to write, and no container to register and resolve the one-method service. A function with the right signature is accepted anywhere the behaviour is wanted, and the compiler checks the fit.
IDisposable, a contract
with several methods, or a type a BCL API requires — a bare function will not satisfy it. The next dive covers
the object expression, which builds that named instance inline without declaring a class.C# habit
A one-method interface (IComparer<T>, a strategy, a callback) carried by a class — declared, then passed or injected by a container.
F# form
A function value of the matching type — string -> string -> int — passed straight to List.sortWith or a higher-order function.
Where this leads
A function serves the one-method case, which is most of them. The cases it cannot serve — a named interface, a
multi-method contract, a type a BCL call demands — need a real instance, and the next dive builds one inline
with an object expression. The dive after covers the seam where an F# function and a .NET
Func meet.
References
Sources
- Microsoft — Functions (F#) — functions as first-class values and the
'a -> 'bfunction type. - FSharp.Core API reference — List module —
List.sortWith,List.map. - Microsoft — Delegates and lambdas (.NET) —
Func<T,R>andIComparer<T>as carriers of one method.
Related in this course
- C2.06.2 — Object expressions — the case a bare function cannot serve.
- C1.03 — Functions as values, currying & partial application — why a function is already a first-class value.
- C2.01 — From classes to modules & functions — methods become module-level functions.