Skip to content

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.

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.

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.

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
}

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 await inside a literal belongs to that literal. The enclosing function is not made asynchronous by it, and needs no await of 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 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 int

A literal that names an explicit wrapper (-> Task<T>, -> ValueTask<T>) is already the callable shape and is not wrapped again.

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: deliberate
let work = func() { await log(e) } // ValueTask: joinable

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.

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 * 2
let score: Score = double

Delegate 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.

&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) = &add
let 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.