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.
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
inttoint, 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 4applies the wrapped function to4. - Action<T>
- the sibling delegate for a function returning nothing —
unitin F# — built the same way.
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.
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
| call | wrapped function | result |
|---|---|---|
| f.Invoke 0 | x + 1 | 1 |
| f.Invoke 4 | x + 1 | 5 |
| f.Invoke 7 | x + 1 | 8 |
| f.Invoke 41 | x + 1 | 42 |
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.
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
- Microsoft — Delegates and lambdas (.NET) — how delegates carry methods as values, and their relation to lambdas.
- Microsoft — System.Func<T,TResult> — the delegate type, its constructor, and
Invoke. - Microsoft — Functions (F#) — F# functions as
FSharpFuncvalues and the function type.
Related in this course
- C2.06.1 — From a single-method interface to a function — the function as the abstraction.
- C1.03 — Functions as values, currying & partial application — the F# function value in language depth.
- C2.06 — The module hub — the three dives together.