Functions & promotion
Functions are the front door of E#. You write them free, at namespace scope, and the compiler attaches them to types where it makes sense — so you get methods without trapping behavior inside a class body.
Declarations
Section titled “Declarations”func add(a: int, b: int) -> int { return a + b}The type comes after the name; the return type after ->. No -> T means the function returns
nothing (void). A top-level func emits as a static method on its namespace class.
returns is a synonym for -> if you prefer the word:
func add(a: int, b: int) returns int { return a + b }Expression-bodied functions
Section titled “Expression-bodied functions”When the body is a single expression, drop the braces and return:
func double(x: int) -> int = x * 2func abs(x: int) -> int = x > 0 ? x : 0 - xfunc log(msg: string) = Console.WriteLine(msg)Generics
Section titled “Generics”CLR reified generics — each instantiation is a real type at runtime:
func identity<T>(value: T) -> T = value
func swap<A, B>(pair: Pair<A, B>) -> Pair<B, A> { return Pair<B, A> { first: pair.second, second: pair.first }}Instance-method promotion
Section titled “Instance-method promotion”This is the one to internalize. A function whose first parameter is a data type
automatically becomes a method on that type. You define behavior next to the data without
nesting it inside the declaration.
data Client { name: string, age: int }
func describe(client: Client) -> string = "{client.name} is {client.age}"
let c = Client { name: "Ada", age: 36 }let s = c.describe() // method callPromotion works across files — the type and the function don’t have to live together. There’s a deliberate rule about call shape:
- A function on a value receiver (
func bump(v: Vec)) is the methodv.bump(). Calling it free asbump(v)is an error (with a fixit pointing atv.bump()). The method travels with its type, reachable wherever the type is in scope. - A function on a pointer receiver (
func bump(v: *Vec)) stays a real free function and joins*Vec’s method set — bothbump(&x)and (where applicable) the method form work. Take a pointer receiver when you want to mutate, or to avoid copying a large struct.
Only data triggers promotion — not choice, enum, or primitives.
Method chaining (fluent)
Section titled “Method chaining (fluent)”Promoted calls chain when a method returns a value the next call lands on. A method that returns
its receiver makes a fluent API — and since a ref data is a reference, returning it hands back
the same object, so each call mutates and returns the one instance, with no re-binding:
ref data Turtle { var x: int, var y: int, init() { self.x = 0 self.y = 0 } }func forward(t: Turtle, n: int) -> Turtle { t.y += n return t } // returns self → chains
let t = Turtle().forward(5).forward(3) // one object threaded through the chainA value data chains too, but each step returns a fresh value — transformation, not mutation
(a.add(b).scaled(2)). A chain may break across lines with a leading dot; a newline before a
. continues the chain:
let t = Turtle() .forward(5) .turn(.right) .forward(3)A method returning Result<T, E> doesn’t chain (the next call would be on a Result) — unwrap each
step with ?, or design the method to return the receiver. See the turtle showcase.
static func — a static class
Section titled “static func — a static class”static func Name { ... } declares a static class (a sibling of the namespace class). Its body
holds fields and functions — the home for grouped helpers and constants.
static func Password { const MIN_LEN = 8 func isStrong(s: string) -> bool = s.Length > MIN_LEN}
func ok() -> bool = Password.isStrong("hunter2hunter2")A let X = <constant> in the body becomes a CLR const; a let X = <expression> becomes a
static readonly; var X is a mutable static; funcs are static methods.
Lambdas & function literals
Section titled “Lambdas & function literals”let dbl = func(x: int) -> int { return x * 2 }let sq = (x) => x * x // arrow form, expression-bodiedlet add = (a, b) => a + bArrow-lambda parameter types are inferred when a delegate type is expected at the call site. Both forms capture surrounding variables (closures), and captures are mutable — writes inside the closure are visible outside, and vice versa.
var total = 0let inc = func() { total = total + 1 }inc()inc()// total == 2Function pointers vs. delegates
Section titled “Function pointers vs. delegates”E# has two ways to pass behavior around, picked by intent:
- Function pointer —
&func, zero allocation, single-target,ldftn+calli. For hot paths and dispatch tables. - Delegate —
Func<>,Action<>, or a nominaldelegate func; heap-allocated and multicast. For framework interop, callbacks, and events.
func addOp(a: int, b: int) -> int = a + blet p = &addOp // function pointerlet n = p(3, 4) // 7 — called via calli, no delegate objectBoth are covered in depth — including nominal delegate func and events — in
Interop & delegates.
Every path must return
Section titled “Every path must return”A non-void function must return on every path; falling through is a hard error rather than a
silent default. The check is exhaustive-match-aware, so a match that covers every variant and
returns in each arm counts as returning — no redundant trailing return needed. See
Control flow & match.