jonnifyfsharp

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.

C2.06.1 · part 1

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 entire IComparer<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.Length orders 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.
C2.06.1 · part 2

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 list the function result ["delta"; "a"; "echo"; "bc"] List.sortWith byLength string -> string -> int ["a"; "bc"; "echo"; "delta"]
List.sortWith byLength ["delta"; "a"; "echo"; "bc"] = ["a"; "bc"; "echo"; "delta"]
Without JavaScript: the same three strategies, by hand
the functioncallresult
byLengthList.sortWith byLength["a"; "bc"; "echo"; "delta"]
byAlphaList.sortWith byAlpha["a"; "bc"; "delta"; "echo"]
toUpperList.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.

C2.06.1 · part 3

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.

When the interface is genuinely named — 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

Related in this course