Functions & Methods
Functions are the unit of computation in Sailfin. They are first-class values, support generics, carry explicit effect annotations, and can be defined as methods on structs. This guide covers every form a function can take.
Function Declarations
Section titled “Function Declarations”The basic form:
fn add(a: number, b: number) -> number { return a + b;}- Parameters use
name: Typenotation. - The return type follows
->after the parameter list. returnexits the function with a value.
Sailfin ships distinct
int(i64) andfloat(f64) types. Integer literals default toint, decimal literals default tofloat, andnumberremains an alias forfloat.
Explicit Returns
Section titled “Explicit Returns”Every function body uses an explicit return statement to yield a value. Short helpers still need the keyword:
fn square(n: number) -> number { return n * n;}
fn max(a: number, b: number) -> number { if a > b { return a; } return b;}Void Functions
Section titled “Void Functions”Functions that perform side effects and return nothing use ![effect] annotations (covered in the next section) and omit the return type or write -> void:
fn greet(name: string) ![io] { print("Hello, {{name}}!");}Multiple Parameters
Section titled “Multiple Parameters”fn format_name(first: string, last: string, title: string) -> string { return "{{title}} {{first}} {{last}}";}
let full = format_name("Jane", "Smith", "Dr.");Pure vs Effectful — Side by Side
Section titled “Pure vs Effectful — Side by Side”A pure function has no effect annotations. It cannot read or write files, make network calls, access the clock, or invoke AI models. The compiler enforces this statically.
// Pure — safe to call anywhere, easy to test, trivially parallelizablefn clamp(value: number, min: number, max: number) -> number { if value < min { return min; } if value > max { return max; } return value;}
// Effectful — declares exactly what it doesfn log_and_clamp(value: number, min: number, max: number) -> number ![io] { let result = clamp(value, min, max); print("clamped {{value}} -> {{result}}"); return result;}Prefer pure functions wherever possible. Reserve effects for the boundary layers (entry points, I/O handlers, API clients).
Effect Signatures
Section titled “Effect Signatures”Every Sailfin function either is pure (no ![]) or declares precisely which capabilities it exercises. This is not documentation — it is enforced by the compiler.
fn read_config(path: string) -> string ![io] { return fs.read(path);}
fn fetch_price(symbol: string) -> number ![net] { let resp = http.get("https://api.example.com/price/{{symbol}}"); return number.parse(resp.body);}
fn analyze(text: string) -> string ![model] { // Call into the `sfn/ai` capsule; the ![model] effect is the capability gate. return ai.complete("gpt4o", text);}Language-level
prompt/model/toolblocks are being migrated to thesfn/aicapsule. The![model]effect stays as the compile-time capability gate. See the roadmap.
The six canonical effects are:
| Effect | Grants access to |
|---|---|
io |
Filesystem (fs.*), console (print, console.*), logging, decorators like @logExecution |
net |
HTTP (http.*), WebSocket (websocket.*), serve |
model |
AI model invocation (sfn/ai capsule) |
gpu |
GPU/accelerator operations, tensor compute |
rand |
Random number generation (rand.*) |
clock |
Wall clock, timers, sleep |
Multiple Effects
Section titled “Multiple Effects”Combine effects in a single annotation. Order does not matter:
fn fetch_and_log(url: string) -> string ![io, net] { let resp = http.get(url); print("Status: {{resp.status}}"); return resp.body;}
fn run_experiment(prompt_text: string) ![io, model, clock] { let start = clock.now(); let result = ai.complete("gpt4o", prompt_text); let elapsed = clock.now() - start; print("Completed in {{elapsed}}ms: {{result}}");}Effect Enforcement
Section titled “Effect Enforcement”If function A directly calls an effectful API (like fs.write or http.get), A must declare the required effect. The compiler checks direct API usage — if you call a helper function B that uses ![io], you must also declare ![io] on A:
fn write_log(msg: string) ![io] { fs.appendFile("app.log", msg + "\n");}
fn process(data: string) ![io] { // OK: process declares ![io], which covers write_log's requirement write_log("Processing: {{data}}");}
// COMPILE ERROR — save() calls write_log() which needs ![io],// but save() only declares ![net]fn save(url: string, data: string) ![net] { let resp = http.post(url, data); write_log("Saved to {{url}}"); // ERROR: missing ![io]}Compiler Fix-It Diagnostics
Section titled “Compiler Fix-It Diagnostics”When a required effect is missing, the compiler produces a precise diagnostic with a suggested fix:
effects.missing: function `save` calls `write_log` which requires ![io], but `save` only declares ![net] = help: add `io` to the effect list: `fn save(url: string, data: string) ![io, net]`Apply the fix, recompile, and the error is gone. The compiler never guesses — it tells you exactly which call site is the problem and which effect to add.
Effects in Tests
Section titled “Effects in Tests”Test blocks also declare effects:
test "reads and parses config" ![io] { let raw = fs.read("test/fixtures/config.toml"); let config = Config.parse(raw); assert config.host == "localhost";}
test "pure addition" { // No effects declared — this test cannot call any effectful function assert add(2, 3) == 5;}Default Parameters
Section titled “Default Parameters”Functions can declare default values for trailing parameters. Callers may omit them:
fn connect(host: string, port: number = 8080, timeout_ms: number = 5000) ![net] { return http.connect(host, port, timeout_ms);}
connect("localhost"); // port=8080, timeout_ms=5000connect("example.com", 443); // port=443, timeout_ms=5000connect("example.com", 443, 10000); // all explicitDefault parameters must come after all required parameters. You cannot skip a defaulted parameter to provide a later one by position — use named argument style in that case:
connect("example.com", timeout_ms: 10000); // port stays at default 8080Generic Functions
Section titled “Generic Functions”Generic functions work over a family of types. The type parameter is declared in angle brackets after the function name:
fn identity<T>(value: T) -> T { return value;}
let n = identity(42); // T inferred as numberlet s = identity("hello"); // T inferred as stringMultiple Type Parameters
Section titled “Multiple Type Parameters”fn pair<A, B>(a: A, b: B) -> (A, B) { return (a, b);}
let p = pair(1, "one");Type Bounds
Section titled “Type Bounds”Generic type bounds (
T: Interface) are on the roadmap and not yet accepted by the parser. For now, write monomorphic helpers or use concrete types; once bounds ship, the intent is:
// PLANNED — not yet availableinterface Comparable { fn compare(self, other: Comparable) -> number;}
fn min<T: Comparable>(a: T, b: T) -> T { if a.compare(b) <= 0 { return a; } return b;}See the roadmap for the generic bounds workstream.
Real Examples: Map, Filter, Find
Section titled “Real Examples: Map, Filter, Find”These patterns illustrate higher-order functions working with Sailfin’s lambda syntax:
fn main() ![io] { let numbers = [1, 2, 3, 4, 5];
let squares = numbers.map(fn(x) -> number { return x * x; }); let sum = squares.reduce(0, fn(acc, x) -> number { return acc + x; });
print("Sum of squares: {{sum}}"); // Outputs: 55}You can also pass a named function where a function value is expected:
fn double(x: number) -> number { return x * 2;}
fn apply(value: number, transformer: (number) -> number) -> number { return transformer(value);}
fn main() ![io] { let result = apply(5, double); print("Result: {{result}}");}Methods on Structs
Section titled “Methods on Structs”Methods are functions associated with a struct type. They are defined inline inside the struct body — Sailfin has no separate impl block today. Methods access the struct instance through a bare self first parameter.
Methods currently take
selfby value. Single-use checking is enforced for owned,Affine<T>, andLinear<T>bindings, while shared/exclusive borrow checking for&Tand&mut Tremains deferred.
struct Counter { value: number;
fn new() -> Counter { return Counter { value: 0 }; }
fn current(self) -> number { return self.value; }
fn increment(self) { self.value = self.value + 1; }
fn add(self, n: number) { self.value = self.value + n; }
fn reset(self) { self.value = 0; }}Usage:
let c: Counter = Counter.new();c.increment();c.increment();c.add(10);print(c.current()); // 12c.reset();print(c.current()); // 0Static Methods (Constructors)
Section titled “Static Methods (Constructors)”Methods with no receiver are called as TypeName.method(). The convention is to use new for the primary constructor:
struct Point { x: number; y: number;
fn new(x: number, y: number) -> Point { return Point { x: x, y: y }; }
fn origin() -> Point { return Point { x: 0.0, y: 0.0 }; }
fn distance(self, other: Point) -> number { let dx = self.x - other.x; let dy = self.y - other.y; return math.sqrt(dx * dx + dy * dy); }
fn translate(self, dx: number, dy: number) { self.x = self.x + dx; self.y = self.y + dy; }}
let p: Point = Point.new(3.0, 4.0);let o: Point = Point.origin();print(p.distance(o)); // 5.0p.translate(1.0, 0.0);print(p.distance(o)); // ~5.099Methods with Effects
Section titled “Methods with Effects”Methods can declare effects just like free functions:
struct Config { host: string; port: number;
fn load(path: string) -> Config ![io] { let raw = fs.read(path); return Config.parse(raw); }
fn save(self, path: string) ![io] { fs.write(path, self.serialize()); }}First-Class Functions
Section titled “First-Class Functions”Functions are values in Sailfin. You can store them in variables, pass them as arguments, and return them from other functions.
Function Types
Section titled “Function Types”The type of a function is written (Param1, Param2, ...) -> ReturnType:
let op: (number, number) -> number = add;let result = op(3, 4); // 7Passing Functions as Arguments
Section titled “Passing Functions as Arguments”fn apply_twice(f: (number) -> number, x: number) -> number { return f(f(x));}
fn double(n: number) -> number { return n * 2;}
let result = apply_twice(double, 3); // double(double(3)) = 12Returning Functions
Section titled “Returning Functions”Closures can capture values from their enclosing scope. That makes returned closures such as a curried adder work today:
fn make_adder(n: number) -> (number) -> number { return fn(x: number) -> number { return x + n; };}
let add5 = make_adder(5);print(add5(10)); // 15print(add5(20)); // 25Closures and Lambdas
Section titled “Closures and Lambdas”Anonymous functions are written with the same fn keyword as named functions — there is no |x| body pipe-closure syntax in Sailfin. A lambda is simply an fn literal you can store in a variable, pass as an argument, or return from another function.
Basic Syntax
Section titled “Basic Syntax”let double = fn(x: number) -> number { return x * 2; };let greet = fn(name: string) -> string { return "Hello, {{name}}!"; };
print(double(5)); // 10print(greet("world")); // "Hello, world!"For a multi-statement body, use additional statements inside the block:
let process = fn(item: string) -> string { let trimmed = item.trim(); let upper = trimmed.to_upper(); return "processed: {{upper}}";};Lambdas in map / filter / reduce
Section titled “Lambdas in map / filter / reduce”let numbers = [1, 2, 3, 4, 5];
let doubled = numbers.map(fn(n) -> number { return n * 2; });let evens = numbers.filter(fn(n) -> boolean { return n % 2 == 0; });let sum = numbers.reduce(0, fn(acc, n) -> number { return acc + n; });Capturing from Outer Scope
Section titled “Capturing from Outer Scope”Closures can capture enclosing variables, including when they are stored or returned. The shipped array combinators also accept capturing callbacks.
For mutation across iterations, use an explicit loop rather than a closure:
let mut total: number = 0;
for n in numbers { total = total + n;}
print(total);Lambdas in Sorting and Transformations
Section titled “Lambdas in Sorting and Transformations”let people = [ Person { name: "Carol", age: 31 }, Person { name: "Alice", age: 25 }, Person { name: "Bob", age: 28 },];
// Sort by agepeople.sort_by(fn(a, b) -> number { return a.age - b.age; });
// Extract nameslet names = people.map(fn(p) -> string { return p.name; });Recursion
Section titled “Recursion”Sailfin functions can call themselves. The compiler does not automatically optimize tail calls yet — deeply recursive algorithms over large inputs should use iterative alternatives or an explicit stack.
Simple Recursion
Section titled “Simple Recursion”fn factorial(n: number) -> number { if n <= 1 { return 1; } return n * factorial(n - 1);}
fn fibonacci(n: number) -> number { match n { 0 => return 0, 1 => return 1, _ => return fibonacci(n - 1) + fibonacci(n - 2), }}Mutual Recursion
Section titled “Mutual Recursion”Two functions can call each other as long as both are declared before the call site, or forward declarations are available:
fn is_even(n: number) -> boolean { if n == 0 { return true; } return is_odd(n - 1);}
fn is_odd(n: number) -> boolean { if n == 0 { return false; } return is_even(n - 1);}Recursive Data Structures
Section titled “Recursive Data Structures”Recursion is natural for tree-shaped data. Self-referential fields use the nullable type operator T?:
struct TreeNode { value: number; left: TreeNode?; right: TreeNode?;}
fn depth(node: TreeNode?) -> number { if node == null { return 0; } let ld = depth(node.left); let rd = depth(node.right); if ld > rd { return 1 + ld; } return 1 + rd;}Decorators
Section titled “Decorators”Decorators apply metadata or behavior modifications to functions. They are written with @ before the function declaration:
@logExecutionfn compute(input: number[]) -> number ![io] { return input.reduce(0.0, fn(acc, x) -> number { return acc + x; });}
@deprecated("Use compute_v2 instead")fn compute_legacy(input: number[]) -> number { return input.reduce(0.0, fn(acc, x) -> number { return acc + x; });}Currently, decorators are parsed and stored as metadata. The @logExecution decorator requires ![io] because the runtime logs entry and exit. The following decorators are recognized:
| Decorator | Effect required | Behavior |
|---|---|---|
@logExecution |
![io] |
Logs function entry and exit |
@deprecated(msg) |
none | Emits a compiler warning at call sites |
@inline |
none | Hint to inline the function at call sites |
@test |
varies | Marks a function as a test (prefer test blocks) |
Custom decorators are planned for a future release as part of the macro system.
Async Functions
Section titled “Async Functions”async fn declarations are parsed, but their return values are structural only:
awaiting an async fn return is not wired into the live type checker yet.
async fn fetch_user(id: string) -> User ![net] { let resp = http.get("https://api.example.com/users/{{id}}"); return User.from_json(resp.body);}For working asynchronous code, spawn a task. spawn fn() -> T { ... } returns
a Task<T>, and await joins that task and unwraps its result:
fn load_dashboard(user_id: string) -> Dashboard ![net] { let user_task = spawn fn() -> User ![net] { let resp = http.get("https://api.example.com/users/{{user_id}}"); return User.from_json(resp.body); }; let orders_task = spawn fn() -> Orders ![net] { return fetch_orders(user_id); };
let user = await user_task; let orders = await orders_task; return Dashboard { user: user, orders: orders };}routine, spawn, await, and channel all ship in v0. Use routine { ... }
to give spawned work a nursery scope; see Concurrency
for task handles, channels, and structured-concurrency patterns.
Function Naming and Dispatch
Section titled “Function Naming and Dispatch”Sailfin does not support traditional overloading — you cannot define two functions with the same name and different parameter types in the same scope. Instead:
Use distinct names:
fn parse_int(s: string) -> number { ... }fn parse_float(s: string) -> number { ... }fn parse_bool(s: string) -> boolean { ... }Use generics when behavior is truly uniform across types:
// PLANNED — generic bounds are on the /roadmap.// Today, write a monomorphic helper per type, or accept the value by concrete type.fn parse<T: Parseable>(s: string) -> T { return T.from_string(s);}Use interface methods when types should define their own behavior. Interfaces are implemented inline on structs with implements — there is no separate impl Trait for Type block:
interface FromString { fn from_string(s: string) -> FromString;}
struct Version implements FromString { major: number; minor: number;
fn from_string(s: string) -> Version { // parse "major.minor" let parts = s.split("."); return Version { major: number.parse(parts[0]), minor: number.parse(parts[1]) }; }}Named Arguments
Section titled “Named Arguments”Sailfin supports named arguments at the call site for any parameter. This is especially useful for boolean flags or when passing the same type multiple times:
fn create_user(name: string, role: string, active: boolean) -> User { ... }
// Without named args — the reader must count parameterslet u1 = create_user("Alice", "admin", true);
// With named args — intent is clearlet u2 = create_user("Alice", role: "admin", active: true);Complete Example
Section titled “Complete Example”This example pulls together effects, generics, methods, closures, and pattern matching:
struct Pipeline<T> { steps: ((T) -> T)[];
fn new() -> Pipeline<T> { return Pipeline<T> { steps: [] }; }
fn add_step(self, step: (T) -> T) { self.steps.append(step); }
fn run(self, input: T) -> T { let mut acc: T = input; for step in self.steps { acc = step(acc); } return acc; }}
fn normalize(s: string) -> string { return s.trim().to_lower();}
fn collapse_spaces(s: string) -> string { return s.split(" ").filter(fn(w) -> boolean { return w.length > 0; }).join(" ");}
fn clean_text(input: string) -> string { let pipe: Pipeline<string> = Pipeline.new(); pipe.add_step(normalize); pipe.add_step(collapse_spaces); return pipe.run(input);}
test "text cleaning pipeline" { let result = clean_text(" Hello World "); assert result == "hello world";}
fn main() ![io] { let raw = " The Quick Brown Fox "; print(clean_text(raw)); // "the quick brown fox"}Next Steps
Section titled “Next Steps”- Types & Structs — Defining custom types, enums, interfaces, and generics
- The Effect System — Deep dive into capability-based security and effect inference
- Error Handling —
try/catch, result types, and propagation - Testing — Writing unit and integration tests
- Ownership & Borrowing — Enforced single-use semantics for owned,
Affine<T>, andLinear<T>bindings, plus the borrow-checking roadmap