Module C2.06
From interfaces to function types
A one-method interface is a function wearing a class. F# drops the class.
A C# developer abstracts behaviour with an interface, and very often it has
exactly one method — IComparer<T>, a strategy, a callback, a
Func<T,R> — implemented by a named class and frequently injected by a container.
In F#, a function value is already that abstraction: an int -> int is a
one-method interface with no ceremony, passed where a comparer or callback is wanted. When the interface is
genuinely named and must be satisfied as a type — IDisposable, a multi-method
contract handed to a BCL API — an object expression implements it inline, no class
declared. And .NET delegates (Func, Action) interoperate with
F# functions directly. This module takes the three cases in turn.
Behaviour as a value, not a type to implement
In C#, to pass behaviour you name an interface and write a class that implements it. For the common case —
a comparer, a strategy, a callback — that interface holds a single method, and the class exists only to carry
that one method to where it is called. F# treats that single method as what it is: a function. A
string -> string -> int is the comparer; an
int -> int is the callback. There is nothing to declare and nothing to
inject — the function is the value, and it is passed directly.
The first dive replaces the single-method interface with a function value: the Strategy
pattern, IComparer<T>, and a Func callback all collapse
to a lambda passed to List.sortWith or a higher-order function. The second covers the
case the function cannot serve — a named, multi-method, or type-required interface such as
IDisposable — with an object expression that builds an instance where
it is used, with no class. The third is the delegate seam: an F# lambda becomes a
Func, a Func is called with .Invoke,
and the rule for keeping a public API friendly to a C# caller.
The three dives · select one
A single-method interface — IComparer<'T>, a strategy, a callback — is one method, so in F# it is one function value. Pass the lambda to List.sortWith or a higher-order function; there is no StrategyImpl class and no container to inject it.
Open this dive · or pick another above
A function is already an abstraction over behaviour. The interface, the class, and the injection are ceremony around a value the language hands you for free.
The habit, in one comparer
The shift is concrete. Where C# declares an IComparer<T> — by hand or inline as
a lambda the Sort overload accepts — F# passes the comparison function straight to
List.sortWith. The "interface" never appears, because a
string -> string -> int is the only thing the sort wanted. Every
// val it : echo below was produced by dotnet fsi; the C#
compiles.
C# — a comparer, declared and passed
var ls = new List<string> { "ccc", "a", "bb" }; ls.Sort((a, b) => a.Length.CompareTo(b.Length)); // ls is now a, bb, ccc // or, for a reusable strategy, a named class: // class ByLength : IComparer<string> { public int Compare(string a, string b) => … }
F# — the comparison function, passed directly
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"]
C# habit
A one-method interface (IComparer<T>, a strategy, a callback) carried by a named class — declared, then passed or injected.
F# form
The method is a function value — string -> string -> int — passed straight to List.sortWith, no class declared.
The three dives
Read them in order. Each stands on valid F# beside the idiomatic C# it replaces. The first shows the common
case — the one-method interface becomes a function value. The second shows the case the function cannot serve —
a named interface — and the object expression that satisfies it inline. The third covers the delegate seam,
where an F# lambda and a .NET Func are the same value crossing in either direction.
From a single-method interface to a function
A comparer, a strategy, a callback — one method each, so one function value. Pass the lambda to List.sortWith.
- the fnwhy a one-method interface needs no class in F#
Object expressions — a real interface, no class
When the interface is named and must be a type (IDisposable, IComparer<int>), an object expression builds the instance inline.
- the type
{ new IComparer<int> with … }, no class declared
Delegates, Func & events
An F# lambda becomes a Func; a Func is called with .Invoke. The boundary rule for a C#-friendly API.
- the seam
System.Func<int,int>↔ an F# function
References
Sources
- Microsoft — Object expressions (F#) — implementing an interface inline with
{ new T with … }. - Microsoft — Functions (F#) — functions as first-class values and the function-type syntax.
- Microsoft — Delegates and lambdas (.NET) —
Func,Action, and how delegates relate to lambdas.
Related in this course
- C2.06.1 — From a single-method interface to a function — the one-method interface as a value.
- 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.