jonnifyfsharp

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

C2.06.1 interface → function C2.06.2 object expressions C2.06.3 delegates & Func the function the named type the seam
C2.06.1 · From a single-method interface to a function

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.

You learn why a one-method interface needs no class in F#

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.

live module hub built dive ready soon specified

References

Sources

Related in this course