Skip to content

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.

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.

When the body is a single expression, drop the braces and return:

func double(x: int) -> int = x * 2
func abs(x: int) -> int = x > 0 ? x : 0 - x
func log(msg: string) = Console.WriteLine(msg)

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.Environment
var starts = 0
generation: int = 1
init {
starts += 1
registerServices()
}
func environment() -> string = initialEnv

Bare 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 getter
let status: string { } // get-only; assign in init
var retries: int { } // get + set
var port: int { set(v) => Max(v, 1) } // custom setter

These 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.Environment

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 }
}

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 call

A 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:

  • Valuefunc (c: T). On a struct, the receiver is a snapshot copy: mutating a field inside does not write back to the caller. On a class, it is the reference itself. In both T’s and *T’s method sets.

  • Pointerfunc (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. *class is illegal — a class is already a reference.

  • Readonlyreadonly func (c: T). Borrows in this: a no-mutation contract, so writing a field through the receiver is rejected at compile time.

  • Static facetfunc (c: static T). This attaches a method to an already-declared static T surface and calls it as T.method(). c is an alias for static members, not an instance. If T has no static declaration, declare one or attach the method as an instance method without static.

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.

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 chain

A 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 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)
}

Browse static examples →

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 = 0
let inc = func() { total = total + 1 }
inc()
inc()
// total == 2

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.
  • DelegateFunc<>, Action<>, or a nominal delegate func; heap-allocated and multicast. For framework interop, callbacks, and events.
func addOp(a: int, b: int) -> int = a + b
let p = &addOp // function pointer
let n = p(3, 4) // 7 — called via calli, no delegate object

Both are covered in depth — including nominal delegate func and events — in Interop & delegates.

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.