Functions
Functions are the primary building blocks in Dovetail. They are first-class values, meaning you can pass them around, return them from other functions, and store them in variables.
5.1 Function Definitions
Section titled “5.1 Function Definitions”Basic Syntax
Section titled “Basic Syntax”Functions are defined using the function keyword:
function greet(): String = "Hello, World!"The function body follows the = sign. Since Dovetail is expression-based, the last expression in the body is the return value.
Parameters and Return Types
Section titled “Parameters and Return Types”Functions can take parameters with type annotations. The return type follows the parameter list:
function add(a: Int32, b: Int32): Int32 = a + b
function formatUser(name: String, age: Int32): String = "$name is $age years old"Unit Return Type
Section titled “Unit Return Type”Functions that don’t return a meaningful value return Unit:
function greet(name: String): Unit = debug("Hello, $name!")You can omit the return type annotation for Unit:
function greet(name: String) = debug("Hello, $name!")Multi-Line Function Bodies
Section titled “Multi-Line Function Bodies”Function bodies can span multiple lines. Each line is an expression, and the last expression is returned:
function calculateTotal(price: Float64, quantity: Int32): Float64 = let subtotal = price * quantity.toFloat64() let tax = subtotal * 0.08 subtotal + taxVisibility
Section titled “Visibility”Top-level functions default to internal (package visibility). Use public to
make them accessible from other packages, or private for file-local helpers:
public function add(a: Int32, b: Int32): Int32 = a + b5.2 Named and Default Arguments
Section titled “5.2 Named and Default Arguments”Named Arguments
Section titled “Named Arguments”When calling a function, you can use named arguments for clarity:
function createUser(name: String, age: Int32, email: String): User = User { name = name; age = age; email = email }
// Using named argumentslet user = createUser(name = "Alice", age = 30, email = "alice@example.com")Named arguments can be in any order:
let user = createUser(email = "bob@example.com", name = "Bob", age = 25)You can mix positional and named arguments, but positional arguments must come first:
let user = createUser("Charlie", age = 28, email = "charlie@example.com")Every parameter must receive exactly one argument. Unknown names, duplicate arguments (including a positional and named argument for the same parameter), missing arguments, and positional arguments after named ones are errors.
Named arguments also work with methods and class constructors. The receiver is
evaluated first, then eager arguments are evaluated once in the order written,
even when that differs from parameter order. ByName arguments remain deferred.
Accepted names come from the declaration visible at the call site. Calls through a trait bound or interface use that contract’s names; calls through a base class use its visible method declaration. Calls through a concrete class use its own method declaration, or the inherited declaration when it does not override the method. An implementation may rename parameters, but those names are not aliases for the contract’s names. Runtime dispatch does not affect argument binding. Renaming a public parameter can therefore break callers that use its name.
Function values, including stored function and bound-method references, accept only positional arguments: their types do not contain parameter names. Positional enum payloads and newtype constructors also have no argument labels. Record construction continues to use its existing field-initializer syntax.
Default Parameter Values (Planned)
Section titled “Default Parameter Values (Planned)”Default parameter values are planned but are not implemented yet. For optional settings today, use a configuration record and a function that constructs it:
record Config = host: String port: Int32
function defaultConfig(host: String): Config = Config { host = host; port = 8080 }
let local = defaultConfig("localhost")let custom = local with { port = 3000 }Record field assignments are distinct from function arguments. The formatter preserves that distinction; it does not introduce named arguments.
The proposed default-parameter syntax is:
function greet(name: String, greeting: String = "Hello"): String = "$greeting, $name!"
greet("Alice") // "Hello, Alice!"greet("Bob", "Hi") // "Hi, Bob!"greet("Charlie", greeting = "Hey") // "Hey, Charlie!"Parameters with defaults must come after required parameters:
function createConfig( host: String, port: Int32 = 8080, timeout: Int32 = 30, debug: Bool = false): Config = Config { host = host; port = port; timeout = timeout; debug = debug }
// Use defaultslet config1 = createConfig("localhost")
// Override some defaultslet config2 = createConfig("localhost", port = 3000, debug = true)Future Linter Rule for Named Arguments
Section titled “Future Linter Rule for Named Arguments”The formatter preserves the caller’s choice of positional or named arguments. Adding argument names requires resolving the called declaration and belongs in a semantic linter.
Dovetail does not yet have a linter. A planned rule would suggest named arguments when the called declaration has more than two explicit parameters, excluding the method receiver:
// You write:createUser("John", 30, "john@example.com")
// A future linter could suggest this fix:createUser(name = "John", age = 30, email = "john@example.com")Additional planned lint rules require named arguments for Boolean and numeric
literals, even in calls with only one or two parameters. For example,
retry(3) would be written as retry(attempts = 3). These lint rules are not
implemented yet. See the future linter design and rule backlog.
5.3 Closures and Lambdas
Section titled “5.3 Closures and Lambdas”Basic Closure Syntax
Section titled “Basic Closure Syntax”Closures (anonymous functions) use the => arrow:
// With type annotations (via expected type)let multiply: (Int32, Int32) => Int32 = (a, b) => a * b
// Single parameter - no parentheses neededlet double = (x: Int32) => x * 2
// Multiple parameters - parentheses requiredlet add = (a: Int32, b: Int32) => a + bMulti-Line Closures
Section titled “Multi-Line Closures”Closure bodies can span multiple lines using indentation:
let process = (x: Int32) => let y = x + 1 let z = y * 2 z + 10Environment Capture
Section titled “Environment Capture”Closures can capture variables from their surrounding scope:
let multiplier = 3let scale = (x: Int32) => x * multiplier
scale(5) // 15Closures capture variables by reference:
function makeCounter(): () => Int32 = let mutable count = 0 () => count = count + 1 count
let counter = makeCounter()counter() // 1counter() // 2counter() // 3Function Types
Section titled “Function Types”Function types describe the signature of a function:
// A function that takes two Int32s and returns an Int32let operation: (Int32, Int32) => Int32 = (a, b) => a + b
// A function that takes no arguments and returns Unitlet action: () => Unit = () => debug("Hello!")
// A function that takes a String and returns a Boollet predicate: (String) => Bool = s => s.length > 05.4 First-Class Function References
Section titled “5.4 First-Class Function References”Named functions can be used as values wherever a function type is expected. Simply use the function name without calling it:
function double(x: Int32): Int32 = x * 2
let f: Int32 => Int32 = doublef(5) // 10Passing Functions as Arguments
Section titled “Passing Functions as Arguments”function identity(x: Int32): Int32 = x
function apply(f: Int32 => Int32, value: Int32): Int32 = f(value)
apply(identity, 42) // 42Module Functions
Section titled “Module Functions”Module functions work the same way:
module Math = function square(x: Int32): Int32 = x * x
let f: Int32 => Int32 = Math.squaref(4) // 16Generic Functions
Section titled “Generic Functions”Generic functions can be used as values when the expected type provides enough information to instantiate the type parameters:
function identity<T>(x: T): T = x
let f: Int32 => Int32 = identity // T inferred as Int32f(42) // 42Overload Disambiguation
Section titled “Overload Disambiguation”When multiple overloads exist, the expected type disambiguates:
function convert(x: Int32): String = "$x"function convert(x: String): Int32 = 0
let f: Int32 => String = convert // selects first overload5.5 Method References
Section titled “5.5 Method References”Bound Method References
Section titled “Bound Method References”When you access a method on an instance without calling it, you get a bound method reference — a function value with self already captured:
record Point = x: Int32 y: Int32
module Point = function getX(self): Int32 = self.x function addX(self, dx: Int32): Int32 = self.x + dx
let p = Point { x = 42; y = 0 }
// Bound reference — self (p) is capturedlet f = p.getX // type: Unit => Int32f() // 42
let g = p.addX // type: Int32 => Int32g(10) // 52The resulting function type excludes self from the parameter list since it is already bound to the object.
Unbound Method References
Section titled “Unbound Method References”You can also reference a method via the type name. This produces a function that takes self as its first parameter:
let f: Point => Int32 = Point.getXlet p = Point { x = 42; y = 0 }f(p) // 42Class Methods
Section titled “Class Methods”Method references work with class instance methods too:
class Counter(public value: Int32) = public function getValue(self: Counter): Int32 = self.value
let c = Counter(99)let f = c.getValuef() // 99Using Method References with Higher-Order Functions
Section titled “Using Method References with Higher-Order Functions”Method references are especially useful when passing to higher-order functions:
function apply(f: Int32 => Int32, arg: Int32): Int32 = f(arg)
let p = Point { x = 42; y = 0 }apply(p.addX, 10) // 525.6 Higher-Order Functions
Section titled “5.6 Higher-Order Functions”Higher-order functions are functions that take other functions as parameters or return functions.
Functions as Parameters
Section titled “Functions as Parameters”function applyTwice(f: (Int32) => Int32, x: Int32): Int32 = f(f(x))
let double = (x: Int32) => x * 2applyTwice(double, 5) // 20 (5 * 2 * 2)Common Higher-Order Functions
Section titled “Common Higher-Order Functions”Dovetail’s standard library provides common higher-order functions for collections:
map - Transform each element:
let numbers = [1, 2, 3, 4, 5]let doubled = numbers.map(x => x * 2) // [2, 4, 6, 8, 10]filter - Keep elements matching a predicate:
let numbers = [1, 2, 3, 4, 5]let evens = numbers.filter(x => x % 2 == 0) // [2, 4]foldLeft - Reduce to a single value:
let numbers = [1, 2, 3, 4, 5]let sum = numbers.foldLeft(0, (acc, x) => acc + x) // 15Returning Functions
Section titled “Returning Functions”Functions can return other functions:
function makeMultiplier(factor: Int32): (Int32) => Int32 = x => x * factor
let triple = makeMultiplier(3)triple(7) // 21Pipeline Style
Section titled “Pipeline Style”Higher-order functions enable a clean pipeline style:
let result = numbers .filter(x => x > 0) .map(x => x * 2) .foldLeft(0, (acc, x) => acc + x)This style is often more readable than nested function calls:
// Equivalent but harder to readlet result = numbers.filter(x => x > 0).map(x => x * 2).foldLeft(0, (acc, x) => acc + x)Summary
Section titled “Summary”- Functions are defined with
function name(params): ReturnType = body - Use named arguments for clarity:
createUser(name = "Alice", age = 30) - Default parameter values are planned; use configuration records for defaults today
- Closures use
=>syntax:x => x * 2or(a, b) => a + b - Closures capture variables from their environment
- Function types describe signatures:
(Int32, Int32) => Int32 - Named functions can be used as values:
let f: Int32 => Int32 = double - Bound method references capture self:
let f = point.getX - Unbound method references include self:
let f: Point => Int32 = Point.getX - Higher-order functions take or return other functions
- Use
map,filter, andfoldLeftfor collection processing
