jonnifyfsharp

Dive C2.06.3

Delegates and Func

A .NET delegate and an F# function are the same value, named two ways.

.NET's own carriers of behaviour are delegates: Func<T,R> for a function that returns a value, Action<T> for one that does not, an EventHandler for an event. A C# API hands you these and expects them back. F# has no delegate keyword to reach for — its functions are the currency — but the two sides meet without friction. An F# lambda becomes a System.Func by construction, and a Func is called with .Invoke. The crossing is a value moving between two names for the same thing.

C2.06.3 · part 1

An F# lambda becomes a Func

A Func<int,int> is .NET's name for a function from int to int. In C#, you assign a lambda to it and call it like a method. In F#, you build one by passing a lambda to the Func constructor, and you call it with .Invoke. The value inside is the same lambda; the wrapper is the delegate type the BCL and C# code speak.

let f = System.Func<int,int>(fun x -> x + 1)
// val f : System.Func<int,int>
f.Invoke 4                        // val it : int = 5

The type of f is System.Func<int,int>, a .NET delegate — not int -> int, the F# function type. The conversion happened at construction. From here, f goes anywhere a C# API asks for a Func<int,int>, and f.Invoke runs the wrapped lambda.

System.Func<int,int>
a .NET delegate type: a function from int to int, the form a C# API exposes.
Func(fun x -> …)
constructing the delegate from an F# lambda — the lambda is wrapped, not rewritten.
.Invoke
the method that runs a delegate; f.Invoke 4 applies the wrapped function to 4.
Action<T>
the sibling delegate for a function returning nothing — unit in F# — built the same way.
C2.06.3 · part 2

The C# the reader knows, beside the F#

In C#, a Func<int,int> is assigned from a lambda and called by writing inc(4). In F#, the same delegate is constructed from a lambda and called with .Invoke. Both produce 5. The C# call syntax hides the Invoke; F# names it.

C# — a Func assigned and called

Func<int, int> inc = x => x + 1;
inc(4);                // 5  (the call hides Invoke)

F# — a Func constructed and invoked

let f = System.Func<int,int>(fun x -> x + 1)
// val f : System.Func<int,int>
f.Invoke 4             // val it : int = 5

Going the other way, an ordinary F# function has type FSharpFunc<_,_>, which a C# caller cannot use as a Func directly. So the rule for a library meant to be called from C# is to type the public parameter as a Func or Action. Inside F#, the function passes as a value as usual; at the public boundary, the delegate type keeps the API friendly to a C# caller. Chapter C2.08 takes the interop seam further.

An F# lambda also converts to a Func automatically when it is passed to a method parameter that already expects one — the explicit System.Func<…>(…) constructor above is for the case where the delegate value is bound on its own.

Cross the bridge

One fixed delegate, f = System.Func<int,int>(fun x -> x + 1). Pick an input. The diagram shows the value crossing from the F# lambda into the Func and out through .Invoke; the readout shows the exact FSI result. The lambda and the delegate hold the same function — the input x returns x + 1 either way.

f.Invoke x · an F# lambda as a .NET delegate

input x F# lambda → Func .Invoke x 0 fun x -> x + 1 wrapped as System.Func<int,int> 1
f.Invoke 0 = 1
Without JavaScript: the same delegate, invoked on each input
callwrapped functionresult
f.Invoke 0x + 11
f.Invoke 4x + 15
f.Invoke 7x + 18
f.Invoke 41x + 142

The lambda and the Func hold one function; the bridge is a change of name, not of value. F# writes the body, .NET carries it as a delegate, and .Invoke runs it.

C2.06.3 · part 3

Functions are the currency

The shift is that F# has nothing matching the delegate keyword and new EventHandler(…) ceremony, because a function is already the value those forms wrap. You meet delegates at the .NET boundary — a Func a library returns, a parameter a C# method expects — and convert with the Func constructor and .Invoke. Inside F#, behaviour is passed as plain functions; the delegate type appears only where the BCL or a C# caller requires it.

C# habit

A delegate type, a Func/Action, or new EventHandler(…) to carry behaviour as a value.

F# form

A function value throughout; a System.Func built and .Invoked only at the boundary where C# expects a delegate.

Where this leads

That completes the move from interfaces to functions: a one-method interface becomes a value, a named interface becomes an object expression, and a .NET delegate becomes a function across the seam. The deeper interop questions — calling the BCL, out parameters as tuples, [<CLIMutable>] records for serializers — are the subject of chapter C2.08. Return to the hub to pick the next module.

References

Sources

Related in this course