Dive C2.06.2
A real interface, no class
When the type is named and a function will not do, write the instance where it is used.
A bare function serves the one-method case, but not every interface collapses to a function. When
the contract is named and a value of that named type is required — IDisposable
for a use block, an IComparer<int> a BCL collection
requires, a contract with several members — F# needs an instance. C# supplies one by declaring a class, often a
private sealed throwaway used in a single place. F# supplies it with an
object expression: { new T with member … } builds an instance of
T inline, no class declared, right where it is read.
{ new T with member … }
An object expression names an interface (or a base type) and supplies its members in one expression. The
result is a value of that interface type, ready to hand to anything that requires one. The simplest case is a
one-method interface that the rest of a system still expects as a type rather than a function —
IDisposable is the everyday example, because a use binding and
many BCL calls require it by name.
let disposable = { new System.IDisposable with member _.Dispose() = printfn "disposed" } // val disposable : System.IDisposable
The type of disposable is System.IDisposable — not a class
name, because no class was declared. The object expression produced an anonymous implementation that satisfies
the interface. Where a C# program would write a small named class to do this once,
{ new … with … } does it where the value is needed.
- { new T with … }
- the object-expression form: name the interface
T, supply its members, get a value of typeT. - member _.M(…)
- a member of the implemented interface; the
_stands for the self-identifier, unused here. - val : IDisposable
- the inferred type is the interface itself — there is no class name because none was written.
- vs. a class
- C# needs a
private sealed classto implement a one-off interface; the object expression skips the declaration.
The C# the reader knows, beside the F#
Take a comparer a BCL API requires as an IComparer<int> — reverse order. C# writes
a class implementing the interface. F# writes the object expression: the same
Compare member, but no class declaration around it. The
compare b a body reverses the usual order, so Compare(1, 2)
returns a positive number where the natural comparer would return a negative one.
C# — a named class to implement one interface
sealed class Reverse : IComparer<int> { public int Compare(int a, int b) => b.CompareTo(a); // reverse } var cmp = new Reverse(); cmp.Compare(1, 2); // 1 (because 2 sorts before 1)
F# — an object expression, no class declared
let cmp = { new System.Collections.Generic.IComparer<int> with member _.Compare(a, b) = compare b a } // reverse order // val cmp : System.Collections.Generic.IComparer<int> cmp.Compare(1, 2) // val it : int = 1
The two are the same interface and the same behaviour. The difference is the ceremony: C# names a type that
exists only to hold one method; F# writes that method directly in an instance. Both values are an
IComparer<int>, and either can be handed to a sort or any BCL API that asks for one.
Sort with the inline comparer
One fixed array, [| 3; 1; 4; 1; 5; 9; 2; 6 |]. Pick which
IComparer<int> the sort uses: the natural one
(compare a b) or the reverse object expression
(compare b a). The readout shows what Compare(1, 2) returns for
the chosen comparer — its sign sets the order — and the array sorted with it.
an IComparer<int> sets the order
| comparer | Compare(1, 2) | sorted array |
|---|---|---|
| compare a b | -1 | [|1; 1; 2; 3; 4; 5; 6; 9|] |
| compare b a | 1 | [|9; 6; 5; 4; 3; 2; 1; 1|] |
The comparer is a value of a named interface type, built where it is used. No class file holds one
method; the Compare body sits in plain view, and the sort reads its sign.
When to reach for it
The rule is narrow, which is the point. Reach for a function first: if the API takes a comparison or a callback
and is happy with a function value, pass the function. Reach for an object expression only when a
named interface is required as a type — a use binding that requires
IDisposable, a BCL method whose parameter is IComparer<T>,
a multi-method contract. The object expression gives the named instance without the named class.
member inside the braces.C# habit
A private sealed class implementing a named interface used in one place — declared, instantiated, discarded.
F# form
{ new IComparer<int> with member _.Compare(a, b) = compare b a } — the instance built inline, no class.
Where this leads
Functions cover the one-method interface; object expressions cover the named one. The remaining piece is the
seam with .NET's own carriers of behaviour — the Func and
Action delegates a C# API hands you and expects back. The next dive shows an F# lambda
and a Func as the same value crossing in either direction.
References
Sources
- Microsoft — Object expressions (F#) —
{ new T with … }, implementing interfaces and base types inline. - Microsoft — System.IDisposable — the one-method interface a
usebinding requires by name. - Microsoft — IComparer<T> — the comparison contract BCL sorts accept.
Related in this course
- C2.06.1 — From a single-method interface to a function — the case a function already serves.
- C2.06.3 — Delegates, Func & events — the .NET delegate seam.
- C2.01 — From classes to modules & functions — when F# does declare a type, and when it does not.