Function values
E# has two distinct callable values. A lambda or method group becomes a delegate when a delegate type is
known at the use site; &name is a function pointer when name resolves to a function. The distinction
is semantic and observable at the CLR boundary, so the compiler never guesses between them.
Grammar
Section titled “Grammar”Lambda = ExplicitLambda | ArrowLambda .ExplicitLambda = "func" "(" [ TypedLambdaParam { "," TypedLambdaParam } ] ")" [ ReturnType ] ( Block | "=" Expr ) .ArrowLambda = "(" [ ArrowLambdaParam { "," ArrowLambdaParam } ] ")" "=>" ( Expr | Block ) .TypedLambdaParam = identifier ":" Type .ArrowLambdaParam = identifier [ ":" Type ] .FuncPtrType = "&" "(" [ TypeList "->" ] Type ")" .FuncAddress = "&" identifier .func is the explicit-signature literal. Its parameters have written types. An omitted -> result is
inferred, not assumed void: it is taken from the destination type when there is one, otherwise from
the body’s returns, and settles to void when no return carries a value. A written -> void is therefore
distinct from writing nothing — the distinction is not observable for an ordinary literal, but it selects
the CLR shape for an asynchronous one (see below). The arrow form never
writes a result type: its result is inferred from its expression or block, or
is constrained by its destination. An arrow parameter may carry a type annotation, but an unannotated arrow
parameter needs a known function shape to acquire its type. Consequently () => 7 can stand in an inferred
local binding, while (x) => x + 1 needs a delegate, function-pointer, generic-call, or equivalent context
that types x. An annotation and a contextual parameter type shall agree.
The two spellings are two ways to write one function-literal semantic form. Neither selects delegate versus function-pointer representation, capture behavior, async behavior, or lifetime; those are determined by the literal’s captures and its destination type. An arrow block is a normal closure body, not a different kind of lambda.
FuncPtrType spells a first-class function pointer: &(int, int -> int), &(string -> void), and
&(-> bool) are respectively a two-parameter, one-parameter void, and zero-parameter shape.
Closure capture
Section titled “Closure capture”A lambda closes over the lexical variables it reads. Captured storage is shared: a mutable captured var
is hoisted into a display object used by the outer function and every closure over that variable. A write in
the closure is therefore visible outside it, and a write outside is visible through the closure.
func makeCounter() -> Func<int> { var total = 0 return func() -> int { total += 1 return total }}Here makeCounter returns the delegate value itself. A func literal can likewise declare -> Func<…> and
return a closure, forming a delegate factory. Return-position conversion, distinct factory environments, and
delegate lifetime are specified in Returning delegates and closure factories.
The mutability rule persists across closure conversion. A closure may read a captured let, but shall not
assign it. A function literal inside a task func body shall not capture an enclosing mutable var
(ES2130); the concurrency boundary must carry shared state through an explicit
channel or other synchronization surface. Capturing an immutable let remains valid.
Target typing and inference
Section titled “Target typing and inference”An unannotated arrow parameter has a type only when a delegate or function-pointer target is known: a typed
binding, parameter, return position, event subscription, or generic call parameter whose type is inferred.
The target’s Invoke signature supplies those parameter types; the arrow body supplies the result constraint.
func parameters already have their source signature, and an annotated arrow parameter contributes the same
constraint. In every case, a concrete target result type constrains the body and an otherwise-open result is
inferred from it.
let twice: Func<int, int> = (x) => x * 2
func mapped<T, U>(value: T, f: Func<T, U>) -> U = f(value)let text = mapped(42, (n) => n.ToString())In the generic call, 42 pins T to int; that target-types n; the inferred lambda result pins U to
string. The complete generic constraint algorithm is in Generics. An unannotated
let f = name has no target type and shall not silently allocate a delegate or choose a function pointer.
The explicit form is useful when the closure’s contract should remain visible independent of its immediate use; the arrow form is useful when the surrounding API already carries that contract:
let parse: Func<string, int> = func(text: string) -> int { return Int32.Parse(text)}
func map<T, U>(value: T, transform: Func<T, U>) -> U = transform(value)let description = map(42, (n) => n.ToString())
let adjust = (n: int) => { let doubled = n * 2 return doubled + 1}Asynchronous function values
Section titled “Asynchronous function values”A literal whose body contains an await is itself asynchronous. It compiles to its own state machine,
exactly as a declaration would, and the same uncolored rules
choose its shape from its result type.
Asyncness does not cross the literal boundary in either direction:
- An
awaitinside a literal belongs to that literal. The enclosing function is not made asynchronous by it, and needs noawaitof its own. - An enclosing function’s asyncness does not reach into the literal. A literal that does not await is an ordinary synchronous function value wherever it appears.
The same rule applies to a spawn body, which is likewise a separate
callable.
func schedule() { let refresh = func() { await cache.reload() } // `refresh` is async; `schedule` is not register(refresh)}The observed delegate type
Section titled “The observed delegate type”The literal’s source result is its unwrapped value; the delegate observes the awaitable the state
machine actually returns. A zero-argument literal with a bare -> int body that awaits is a
Func<ValueTask<int>>, not a Func<int> — invoking a delegate cannot suspend the caller, so the wrapper
has to be visible at the call site.
let load = func() -> int { return await fetch() } // Func<ValueTask<int>>let value = await load.Invoke() // yields intA literal that names an explicit wrapper (-> Task<T>, -> ValueTask<T>) is already the callable shape and
is not wrapped again.
Async-void is never implicit
Section titled “Async-void is never implicit”A written -> void on an awaiting literal produces CLR async-void: the call returns immediately, the
caller cannot join it, and a fault is raised on the captured context rather than surfacing to anyone. That
shape exists for event-handler contracts and shall be requested explicitly.
An omitted result type never selects it. An awaiting literal with no annotation and no value-returning
return is ValueTask-shaped — joinable, and its faults observable.
let handler: EventHandler = func(sender: object, e: EventArgs) -> void { await log(e) } // async-void: deliberatelet work = func() { await log(e) } // ValueTask: joinableCaptures
Section titled “Captures”Capture rules are unchanged by asyncness — a captured local lives in the closure, and the state machine
spills it across suspension points like any other live value. Inside a task func or spawn body the
ES2130 restriction still applies: a literal there shall not capture a
surrounding mutable var.
Delegates
Section titled “Delegates”A named function or a lambda converts to a delegate only in a delegate-typed position. The conversion binds
directly to the actual target method where possible; a capturing lambda instead materializes its closure
object. Nominal delegate func declarations, method-group conversion, events, and BCL delegate interop
are specified in Delegates & events.
delegate func Score(value: int) -> int
func double(value: int) -> int = value * 2let score: Score = doubleDelegate identity is nominal. Score and Func<int, int> have compatible shapes but are different CLR
delegate types; a method group can be converted to either when its target is known, but an existing value
of one does not implicitly become the other.
Function pointers
Section titled “Function pointers”&name resolves by what name denotes. If it resolves to a function, it produces a function pointer;
if it resolves to storage, it is the address-of form described in Pointers & by-ref.
Function pointers are first-class values: they may be fields, parameters, locals, returns, and direct call
targets. The compiler shall verify their call signature.
func add(left: int, right: int) -> int = left + right
let op: &(int, int -> int) = &addlet answer = op(20, 22)The pointer lowering is ldftn plus calli: zero allocation and exactly one target. A delegate is the
interop tier — allocation-capable, multicast, and suitable for BCL callbacks or events. Choose a function
pointer for a hot, single-target dispatch table; choose a delegate for framework-facing callback identity
or multicast behavior.
See also
Section titled “See also”- Functions — the complete functions reference.
- Delegates & events — nominal delegate declarations and event semantics.
- Pointers & by-ref — address-of versus
&functiondisambiguation. - Awaiting and async return shapes — the uncolored rules an asynchronous literal shares with a declaration.