Functions & methods
Functions are the front door of E#. You write them free, at namespace scope; a function becomes a method on a type by giving it a receiver — a Go-style block before the name — so you get methods without trapping behavior inside a class body.
This is the fast path: declarations, methods, static facets, and function values in one place. When you need the exact grammar, overload rule, or CLR boundary, use the Functions specification.
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.
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)Namespace state and one-time setup
Section titled “Namespace state and one-time setup”Free functions share namespace-host state directly. let is read-only after initialization; var is
assignable. Use the namespace init block for synchronous setup that must happen once before the first
free-function or state access:
let initialEnv = AppConfig.Environmentvar starts = 0generation: int = 1
init { starts += 1 registerServices()}
func environment() -> string = initialEnvBare typed namespace state is a direct mutable static field; namespace let and var are properties. State
initializes before the init body. The block is lazy per namespace host, runs once, may call
later-declared functions, and cannot await, yield, or return. It is not eager assembly-load code.
The namespace host also accepts the ordinary E# property suffixes:
let environment: string => AppConfig.Environment // computed getterlet status: string { } // get-only; assign in initvar retries: int { } // get + setvar port: int { set(v) => Max(v, 1) } // custom setterThese are real static CLR properties, not public fields dressed up in the guide: computed properties have no backing field; stored properties have private static storage and static accessors.
Inside a function, an explicitly typed mutable local may use field order when that reads better:
currentEnv: string = AppConfig.EnvironmentGenerics
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 }}Methods (receivers)
Section titled “Methods (receivers)”This is the one to internalize. A function with a receiver block is a method on that type. The
receiver is named and written like a parameter, in a block before the method name (E#‘s colon
form — name: Type, like Go but with the :). You define behavior next to the data without nesting
it inside the declaration.
struct Client { name: string, age: int }
func (c: Client) describe() -> string = "{c.name} is {c.age}"
let c = Client { name: "Ada", age: 36 }let s = c.describe() // method callA method attaches namespace-locally: the method and its receiver type must share a namespace (across files is fine — a namespace spans files). A receiver type from a different namespace is an error. There are three receiver kinds:
-
Value —
func (c: T). On astruct, the receiver is a snapshot copy: mutating a field inside does not write back to the caller. On aclass, it is the reference itself. In bothT’s and*T’s method sets. -
Pointer —
func (c: *T). Operates through the pointer and mutates in place (ref this). In*T’s method set only. Reach for it to mutate, or to avoid copying a large struct.*classis illegal — a class is already a reference. -
Readonly —
readonly func (c: T). Borrowsin this: a no-mutation contract, so writing a field through the receiver is rejected at compile time. -
Static facet —
func (c: static T). This attaches a method to an already-declaredstatic Tsurface and calls it asT.method().cis an alias for static members, not an instance. IfThas no static declaration, declare one or attach the method as an instance method withoutstatic.
A method is method-only, the Go rule: the free-call spelling describe(c) / bump(v) is a hard
error (ES2142) with a fixit pointing at c.describe() / v.bump().
A bare first-param function with no receiver block (func describe(c: Client)) is an ordinary
free function — called describe(c), never c.describe(). A receiver is what makes a method; a plain
parameter does not.
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 class is a reference, returning it hands back
the same object, so each call mutates and returns the one instance, with no re-binding:
class Turtle { var x: int, var y: int, init() { self.x = 0 self.y = 0 } }func (t: Turtle) forward(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 struct 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 — a static class
Section titled “static — a static class”static 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 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; initialized let and var declarations become
static properties with compiler-managed storage (let read/construction, var read/write); funcs are
static methods.
A static may share its name with a non-generic class. They emit as one CLR type: the class
contributes constructors and instance members, while the static block contributes static members. This lets an
API offer both TaskScope(token) and TaskScope.RunAsync(...) without inventing a second name.
class Meter { value: int init(value: int) { self.value = value }}static Meter { func zero() -> Meter = Meter(0)}Lambdas & function literals
Section titled “Lambdas & function literals”let double = func(value: int) -> int { return value * 2 } // explicit contract
func map<T, U>(value: T, transform: Func<T, U>) -> U = transform(value)let text = map(21, (n) => n.ToString()) // shape comes from map
let adjust = (n: int) => { // optional parameter annotation let doubled = n * 2 return doubled + 1}Use func when the closure’s signature belongs in the source. Use => when the result should be inferred
and the surrounding call already explains the callback shape. Arrow bodies may be expressions or blocks;
parameter annotations are optional. Both forms have identical closure behavior. For target typing,
delegate/function-pointer selection, async rules, and capture lifetime, see
Function values.
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.