Skip to content

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.

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 }

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)

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

Promotion 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 method v.bump(). Calling it free as bump(v) is an error (with a fixit pointing at v.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 — both bump(&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.

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 chain

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

Browse static func examples →

let dbl = func(x: int) -> int { return x * 2 }
let sq = (x) => x * x // arrow form, expression-bodied
let add = (a, b) => a + b

Arrow-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 = 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.